From a3fcb11be6c431dafac6fae2f411ee7ae84b39f8 Mon Sep 17 00:00:00 2001 From: Jonathan Rainville Date: Mon, 13 Jan 2025 15:35:25 -0500 Subject: [PATCH] feat(onboarding): add AC notif for importing old accounts Fixes #17028 When an old user imports an account, we now fetch the backups in the background and show an AC notification. When the fetch is successful, the AC notif switches to a success message. If after a timeout we detect that we didn't fetch anything or just part, we show an error and the possibility to try again. --- .../main/activity_center/controller.nim | 19 ++- .../main/activity_center/io_interface.nim | 3 + .../modules/main/activity_center/module.nim | 18 +- src/app/modules/main/activity_center/view.nim | 3 + src/app/modules/main/module.nim | 2 +- src/app/modules/onboarding/io_interface.nim | 1 + src/app/modules/onboarding/module.nim | 14 +- src/app/modules/startup/controller.nim | 4 +- .../profile_fetching_announcement_state.nim | 2 +- .../activity_center/dto/notification.nim | 8 + .../service/general/async_tasks.nim | 16 ++ src/app_service/service/general/service.nim | 37 ++++- .../popups/ActivityCenterPopup.qml | 22 +++ .../stores/ActivityCenterStore.qml | 10 +- .../ActivityNotificationProfileFetching.qml | 157 ++++++++++++++++++ ui/main.qml | 7 +- vendor/status-go | 2 +- 17 files changed, 291 insertions(+), 34 deletions(-) create mode 100644 src/app_service/service/general/async_tasks.nim create mode 100644 ui/app/mainui/activitycenter/views/ActivityNotificationProfileFetching.qml diff --git a/src/app/modules/main/activity_center/controller.nim b/src/app/modules/main/activity_center/controller.nim index a96394cbd10..37b40eef66e 100644 --- a/src/app/modules/main/activity_center/controller.nim +++ b/src/app/modules/main/activity_center/controller.nim @@ -2,12 +2,13 @@ import ./io_interface import ../../../global/app_signals import ../../../core/eventemitter -import ../../../../app_service/service/activity_center/service as activity_center_service -import ../../../../app_service/service/contacts/service as contacts_service -import ../../../../app_service/service/message/service as message_service -import ../../../../app_service/service/community/service as community_service -import ../../../../app_service/service/chat/service as chat_service -import ../../../../app_service/service/devices/service as devices_service +import app_service/service/activity_center/service as activity_center_service +import app_service/service/contacts/service as contacts_service +import app_service/service/message/service as message_service +import app_service/service/community/service as community_service +import app_service/service/chat/service as chat_service +import app_service/service/devices/service as devices_service +import app_service/service/general/service as general_service type Controller* = ref object of RootObj @@ -19,6 +20,7 @@ type chatService: chat_service.Service communityService: community_service.Service devicesService: devices_service.Service + generalService: general_service.Service proc newController*( delegate: io_interface.AccessInterface, @@ -29,6 +31,7 @@ proc newController*( chatService: chat_service.Service, communityService: community_service.Service, devicesService: devices_service.Service, + generalService: general_service.Service, ): Controller = result = Controller() result.delegate = delegate @@ -39,6 +42,7 @@ proc newController*( result.chatService = chatService result.communityService = communityService result.devicesService = devicesService + result.generalService = generalService proc delete*(self: Controller) = discard @@ -169,3 +173,6 @@ proc getActivityCenterReadType*(self: Controller): ActivityCenterReadType = proc enableInstallationAndSync*(self: Controller, installationId: string) = self.devicesService.enableInstallationAndSync(installationId) + +proc tryFetchingAgain*(self: Controller) = + self.generalService.asyncFetchWakuBackupMessages() diff --git a/src/app/modules/main/activity_center/io_interface.nim b/src/app/modules/main/activity_center/io_interface.nim index 41bd7f60f4a..2a0ac3afd78 100644 --- a/src/app/modules/main/activity_center/io_interface.nim +++ b/src/app/modules/main/activity_center/io_interface.nim @@ -113,3 +113,6 @@ method setActivityGroupCounters*(self: AccessInterface, counters: Table[Activity method enableInstallationAndSync*(self: AccessInterface, installationId: string) {.base.} = raise newException(ValueError, "No implementation available") + +method tryFetchingAgain*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/activity_center/module.nim b/src/app/modules/main/activity_center/module.nim index 18214734979..41f881e09e1 100644 --- a/src/app/modules/main/activity_center/module.nim +++ b/src/app/modules/main/activity_center/module.nim @@ -8,12 +8,13 @@ import ../../shared_models/message_item_qobject as msg_item_qobj import ../../../global/global_singleton import ../../../global/app_sections_config as conf import ../../../core/eventemitter -import ../../../../app_service/service/activity_center/service as activity_center_service -import ../../../../app_service/service/contacts/service as contacts_service -import ../../../../app_service/service/message/service as message_service -import ../../../../app_service/service/chat/service as chat_service -import ../../../../app_service/service/community/service as community_service -import ../../../../app_service/service/devices/service as devices_service +import app_service/service/activity_center/service as activity_center_service +import app_service/service/contacts/service as contacts_service +import app_service/service/message/service as message_service +import app_service/service/chat/service as chat_service +import app_service/service/community/service as community_service +import app_service/service/devices/service as devices_service +import app_service/service/general/service as general_service export io_interface @@ -35,6 +36,7 @@ proc newModule*( chatService: chat_service.Service, communityService: community_service.Service, devicesService: devices_service.Service, + generalService: general_service.Service, ): Module = result = Module() result.delegate = delegate @@ -49,6 +51,7 @@ proc newModule*( chatService, communityService, devicesService, + generalService, ) result.moduleLoaded = false @@ -288,3 +291,6 @@ method setActivityGroupCounters*(self: Module, counters: Table[ActivityCenterGro method enableInstallationAndSync*(self: Module, installationId: string) = self.controller.enableInstallationAndSync(installationId) + +method tryFetchingAgain*(self: Module) = + self.controller.tryFetchingAgain() diff --git a/src/app/modules/main/activity_center/view.nim b/src/app/modules/main/activity_center/view.nim index 7828086e0e8..7af99b558ae 100644 --- a/src/app/modules/main/activity_center/view.nim +++ b/src/app/modules/main/activity_center/view.nim @@ -207,3 +207,6 @@ QtObject: proc enableInstallationAndSync*(self: View, installationId: string) {.slot.} = self.delegate.enableInstallationAndSync(installationId) + + proc tryFetchingAgain*(self: View) {.slot.} = + self.delegate.tryFetchingAgain() diff --git a/src/app/modules/main/module.nim b/src/app/modules/main/module.nim index f217f2231bd..39d84f142dd 100644 --- a/src/app/modules/main/module.nim +++ b/src/app/modules/main/module.nim @@ -231,7 +231,7 @@ proc newModule*[T]( networkService, tokenService) result.gifsModule = gifs_module.newModule(result, events, gifService) result.activityCenterModule = activity_center_module.newModule(result, events, activityCenterService, contactsService, - messageService, chatService, communityService, devicesService) + messageService, chatService, communityService, devicesService, generalService) result.communitiesModule = communities_module.newModule(result, events, communityService, contactsService, communityTokensService, networkService, transactionService, tokenService, chatService, walletAccountService, keycardService) result.appSearchModule = app_search_module.newModule(result, events, contactsService, chatService, communityService, diff --git a/src/app/modules/onboarding/io_interface.nim b/src/app/modules/onboarding/io_interface.nim index 68346f1e3d9..c853f8a5799 100644 --- a/src/app/modules/onboarding/io_interface.nim +++ b/src/app/modules/onboarding/io_interface.nim @@ -46,5 +46,6 @@ type DelegateInterface* = concept c c.onboardingDidLoad() c.appReady() + c.userLoggedIn() c.finishAppLoading() c.userLoggedIn() diff --git a/src/app/modules/onboarding/module.nim b/src/app/modules/onboarding/module.nim index 34fa87f28b6..b4fa551ea30 100644 --- a/src/app/modules/onboarding/module.nim +++ b/src/app/modules/onboarding/module.nim @@ -138,7 +138,7 @@ method finishOnboardingFlow*[T](self: Module[T], flowInt: int, dataJson: string) seedPhrase, recoverAccount = true, keycardInstanceUID = "", - ) + ) of SecondaryFlow.LoginWithSyncing: # The pairing was already done directly through inputConnectionStringForBootstrapping, we can login self.controller.loginLocalPairingAccount( @@ -169,14 +169,18 @@ proc finishAppLoading2[T](self: Module[T]) = self.delegate.finishAppLoading() -method onNodeLogin*[T](self: Module[T], error: string, account: AccountDto, settings: SettingsDto) = - if error.len != 0: - # TODO: Handle error - echo "ERROR from onNodeLogin: ", error +method onNodeLogin*[T](self: Module[T], err: string, account: AccountDto, settings: SettingsDto) = + if err.len != 0: + error "error from onNodeLogin", err return self.controller.setLoggedInAccount(account) + let err2 = self.delegate.userLoggedIn() + if err2.len != 0: + error "error from userLoggedIn", err2 + return + if self.localPairingStatus != nil and self.localPairingStatus.installation != nil and self.localPairingStatus.installation.id != "": # We tried to login by pairing, so finilize the process self.controller.finishPairingThroughSeedPhraseProcess(self.localPairingStatus.installation.id) diff --git a/src/app/modules/startup/controller.nim b/src/app/modules/startup/controller.nim index 47be149f07e..eeb1d90c627 100644 --- a/src/app/modules/startup/controller.nim +++ b/src/app/modules/startup/controller.nim @@ -219,8 +219,8 @@ proc generateImage*(self: Controller, imageUrl: string, aX: int, aY: int, bX: in ) return img.uri -proc fetchWakuMessages*(self: Controller) = - self.generalService.fetchWakuMessages() +proc asyncFetchWakuBackupMessages*(self: Controller) = + self.generalService.asyncFetchWakuBackupMessages() proc getCroppedProfileImage*(self: Controller): string = return self.tmpProfileImageDetails.croppedImage diff --git a/src/app/modules/startup/internal/profile_fetching_announcement_state.nim b/src/app/modules/startup/internal/profile_fetching_announcement_state.nim index 9d758549e7b..02d99819167 100644 --- a/src/app/modules/startup/internal/profile_fetching_announcement_state.nim +++ b/src/app/modules/startup/internal/profile_fetching_announcement_state.nim @@ -11,7 +11,7 @@ proc delete*(self: ProfileFetchingAnnouncementState) = method executePrimaryCommand*(self: ProfileFetchingAnnouncementState, controller: Controller) = if self.flowType == FlowType.FirstRunOldUserImportSeedPhrase or self.flowType == FlowType.FirstRunOldUserKeycardImport: - controller.fetchWakuMessages() + controller.asyncFetchWakuBackupMessages() method getNextPrimaryState*(self: ProfileFetchingAnnouncementState, controller: Controller): State = if self.flowType == FlowType.FirstRunOldUserImportSeedPhrase or diff --git a/src/app_service/service/activity_center/dto/notification.nim b/src/app_service/service/activity_center/dto/notification.nim index 70605e3deef..d99b78a49a7 100644 --- a/src/app_service/service/activity_center/dto/notification.nim +++ b/src/app_service/service/activity_center/dto/notification.nim @@ -34,6 +34,10 @@ type ActivityCenterNotificationType* {.pure.}= enum CommunityUnbanned = 22 NewInstallationReceived = 23 NewInstallationCreated = 24 + BackupSyncingFetching = 25 + BackupSyncingSuccess = 26 + BackupSyncingPartialFailure = 27 + BackupSyncingFailure = 28 type ActivityCenterGroup* {.pure.}= enum All = 0, @@ -178,6 +182,10 @@ proc activityCenterNotificationTypesByGroup*(group: ActivityCenterGroup) : seq[i ActivityCenterNotificationType.CommunityUnbanned.int, ActivityCenterNotificationType.NewInstallationReceived.int, ActivityCenterNotificationType.NewInstallationCreated.int, + ActivityCenterNotificationType.BackupSyncingFetching.int, + ActivityCenterNotificationType.BackupSyncingSuccess.int, + ActivityCenterNotificationType.BackupSyncingPartialFailure.int, + ActivityCenterNotificationType.BackupSyncingFailure.int, ] of ActivityCenterGroup.Mentions: return @[ActivityCenterNotificationType.Mention.int] diff --git a/src/app_service/service/general/async_tasks.nim b/src/app_service/service/general/async_tasks.nim new file mode 100644 index 00000000000..75bffbad704 --- /dev/null +++ b/src/app_service/service/general/async_tasks.nim @@ -0,0 +1,16 @@ + +type + AsyncFetchBackupWakuMessagesTaskArg = ref object of QObjectTaskArg + +proc asyncFetchWakuBackupMessagesTask(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[AsyncFetchBackupWakuMessagesTaskArg](argEncoded) + try: + let response = status_mailservers.requestAllHistoricMessagesWithRetries(forceFetchingBackup = true) + arg.finish(%* { + "response": response, + "error": "", + }) + except Exception as e: + arg.finish(%* { + "error": e.msg, + }) diff --git a/src/app_service/service/general/service.nim b/src/app_service/service/general/service.nim index 03a04b6ed4a..70c6bb06139 100644 --- a/src/app_service/service/general/service.nim +++ b/src/app_service/service/general/service.nim @@ -6,8 +6,13 @@ import ../../../app/core/eventemitter import ../../../app/core/tasks/[qt, threadpool] import ../../../constants as app_constants +from app_service/service/activity_center/service import SIGNAL_ACTIVITY_CENTER_NOTIFICATIONS_LOADED, ActivityCenterNotificationsArgs +from app_service/service/activity_center/dto/notification import parseActivityCenterNotifications + import ../accounts/dto/accounts +include async_tasks + const TimerIntervalInMilliseconds = 1000 # 1 second const SIGNAL_GENERAL_TIMEOUT* = "timeoutSignal" @@ -37,7 +42,12 @@ QtObject: createDir(app_constants.ROOTKEYSTOREDIR) proc startMessenger*(self: Service) = - discard status_general.startMessenger() + let response = status_general.startMessenger() + if response.result.contains("activityCenterNotifications"): + let notifications = JsonNode(%{"notifications": response.result["activityCenterNotifications"]}) + let activityCenterNotificationsTuple = parseActivityCenterNotifications(notifications) + self.events.emit(SIGNAL_ACTIVITY_CENTER_NOTIFICATIONS_LOADED, + ActivityCenterNotificationsArgs(activityCenterNotifications: activityCenterNotificationsTuple[1])) proc logout*(self: Service) = discard status_general.logout() @@ -87,13 +97,28 @@ QtObject: else: self.runTimer() - proc fetchWakuMessages*(self: Service) = + proc asyncFetchWakuBackupMessages*(self: Service) = + let arg = AsyncFetchBackupWakuMessagesTaskArg( + tptr: asyncFetchWakuBackupMessagesTask, + vptr: cast[uint](self.vptr), + slot: "onFetchWakuBackupMessagesDone", + ) + self.threadpool.start(arg) + + proc onFetchWakuBackupMessagesDone(self: Service, response: string) {.slot.} = try: - let response = status_mailservers.requestAllHistoricMessagesWithRetries(forceFetchingBackup = true) - if(not response.error.isNil): - error "could not set display name" + let rpcResponseObj = response.parseJson + + if rpcResponseObj{"error"}.kind != JNull and rpcResponseObj{"error"}.getStr != "": + raise newException(CatchableError, rpcResponseObj{"error"}.getStr) + + if rpcResponseObj["response"]["result"].contains("activityCenterNotifications"): + let notifications = JsonNode(%{"notifications": rpcResponseObj["response"]["result"]["activityCenterNotifications"]}) + let activityCenterNotificationsTuple = parseActivityCenterNotifications(notifications) + self.events.emit(SIGNAL_ACTIVITY_CENTER_NOTIFICATIONS_LOADED, + ActivityCenterNotificationsArgs(activityCenterNotifications: activityCenterNotificationsTuple[1])) except Exception as e: - error "error: ", procName="fetchWakuMessages", errName = e.name, errDesription = e.msg + error "error:", procName="asyncFetchWakuBackupMessages", errName = e.name, errDesription = e.msg proc backupData*(self: Service): int64 = try: diff --git a/ui/app/mainui/activitycenter/popups/ActivityCenterPopup.qml b/ui/app/mainui/activitycenter/popups/ActivityCenterPopup.qml index bdc2c02a2ac..e9e7695a254 100644 --- a/ui/app/mainui/activitycenter/popups/ActivityCenterPopup.qml +++ b/ui/app/mainui/activitycenter/popups/ActivityCenterPopup.qml @@ -152,6 +152,11 @@ Popup { case ActivityCenterStore.ActivityCenterNotificationType.NewInstallationReceived: case ActivityCenterStore.ActivityCenterNotificationType.NewInstallationCreated: return newDeviceDetectedComponent + case ActivityCenterStore.ActivityCenterNotificationType.BackupSyncingFetching: + case ActivityCenterStore.ActivityCenterNotificationType.BackupSyncingSuccess: + case ActivityCenterStore.ActivityCenterNotificationType.BackupSyncingPartialFailure: + case ActivityCenterStore.ActivityCenterNotificationType.BackupSyncingFailure: + return backupSyncingComponent default: return null } @@ -338,6 +343,23 @@ Popup { } } + Component { + id: backupSyncingComponent + + ActivityNotificationProfileFetching { + id: activityNotificationProfileFetching + type: setType(notification) + filteredIndex: parent.filteredIndex + notification: parent.notification + onTryAgainClicked: { + // Force the type back to in progress since the fetching is async and the state will not update imediately + activityNotificationProfileFetching.type = ActivityNotificationProfileFetching.FetchingState.Fetching + + root.activityCenterStore.tryFetchingAgain() + } + } + } + Component { id: communityTokenReceivedComponent diff --git a/ui/app/mainui/activitycenter/stores/ActivityCenterStore.qml b/ui/app/mainui/activitycenter/stores/ActivityCenterStore.qml index e1ba5f8731a..662b0c20aac 100644 --- a/ui/app/mainui/activitycenter/stores/ActivityCenterStore.qml +++ b/ui/app/mainui/activitycenter/stores/ActivityCenterStore.qml @@ -42,7 +42,11 @@ QtObject { CommunityBanned = 21, CommunityUnbanned = 22, NewInstallationReceived = 23, - NewInstallationCreated = 24 + NewInstallationCreated = 24, + BackupSyncingFetching = 25, + BackupSyncingSuccess = 26, + BackupSyncingPartialFailure = 27, + BackupSyncingFailure = 28 } enum ActivityCenterReadType { @@ -123,4 +127,8 @@ QtObject { function enableInstallationAndSync(installationId) { root.activityCenterModuleInst.enableInstallationAndSync(installationId) } + + function tryFetchingAgain() { + root.activityCenterModuleInst.tryFetchingAgain() + } } diff --git a/ui/app/mainui/activitycenter/views/ActivityNotificationProfileFetching.qml b/ui/app/mainui/activitycenter/views/ActivityNotificationProfileFetching.qml new file mode 100644 index 00000000000..f42d61050a4 --- /dev/null +++ b/ui/app/mainui/activitycenter/views/ActivityNotificationProfileFetching.qml @@ -0,0 +1,157 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Components 0.1 + +import shared 1.0 +import shared.panels 1.0 +import utils 1.0 +import mainui.activitycenter.stores 1.0 + + +ActivityNotificationBase { + id: root + + required property int type // Possible values [FetchingState] + + signal tryAgainClicked + + function setType(notification) { + if (notification) { + switch (notification.notificationType) { + case ActivityCenterStore.ActivityCenterNotificationType.BackupSyncingFetching: + return ActivityNotificationProfileFetching.FetchingState.Fetching + case ActivityCenterStore.ActivityCenterNotificationType.BackupSyncingSuccess: + return ActivityNotificationProfileFetching.FetchingState.Success + case ActivityCenterStore.ActivityCenterNotificationType.BackupSyncingPartialFailure: + return ActivityNotificationProfileFetching.FetchingState.PartialFailure + case ActivityCenterStore.ActivityCenterNotificationType.BackupSyncingFailure: + return ActivityNotificationProfileFetching.FetchingState.Failure + } + } + return ActivityNotificationProfileFetching.FetchingState.Unknown + } + + enum FetchingState { + Unknown, + Fetching, + Success, + PartialFailure, + Failure + } + + QtObject { + id: d + + property string title: qsTr("Fetching profile details") + property string info: "" + property string badgeName: "" + property string ctaText: "" + property string badgeColor: "" + } + + bodyComponent: RowLayout { + spacing: 8 + + StatusSmartIdenticon { + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + Layout.alignment: Qt.AlignTop + Layout.leftMargin: Theme.padding + Layout.topMargin: 2 + + asset { + width: 24 + height: width + name: "download" + color: Theme.palette.primaryColor1 + bgWidth: 40 + bgHeight: 40 + bgColor: Theme.palette.primaryColor3 + } + + bridgeBadge.visible: true + bridgeBadge.border.width: 2 + bridgeBadge.color: d.badgeColor + bridgeBadge.image.source: Theme.svg(d.badgeName) + } + + ColumnLayout { + spacing: 2 + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + + StatusMessageHeader { + Layout.fillWidth: true + displayNameLabel.text: d.title + timestamp: root.notification.timestamp + } + + RowLayout { + spacing: Theme.padding + + StatusBaseText { + Layout.fillWidth: true + text: d.info + font.italic: true + wrapMode: Text.WordWrap + color: Theme.palette.baseColor1 + } + } + } + } + + ctaComponent: StatusFlatButton { + size: StatusBaseButton.Size.Small + text: d.ctaText + onClicked: { + root.tryAgainClicked() + } + } + + states: [ + State { + when: root.type === ActivityNotificationProfileFetching.FetchingState.Fetching + PropertyChanges { + target: d + info: qsTr("Fetching all data may take some time") + badgeName: "dotsLoadings" + ctaText: "" + badgeColor: Theme.palette.baseColor3 + } + }, + State { + when: root.type === ActivityNotificationProfileFetching.FetchingState.Success + PropertyChanges { + target: d + info: qsTr("Profile details fetched successfully") + badgeName: "check"// TODO fix icon it looks bad + ctaText: "" + badgeColor: Theme.palette.successColor1 + } + }, + State { + when: root.type === ActivityNotificationProfileFetching.FetchingState.PartialFailure + PropertyChanges { + target: d + info: qsTr("Some profile details could not be fetched") + badgeName: "exclamation_outline" // TODO fix icon it looks bad + ctaText: qsTr("Try again") + badgeColor: Theme.palette.dangerColor1 + } + }, + State { + when: root.type === ActivityNotificationProfileFetching.FetchingState.Failure + PropertyChanges { + target: d + info: qsTr("Profile details could not be fetched") + badgeName: "exclamation_outline" // TODO fix icon it looks bad + ctaText: qsTr("Try again") + badgeColor: Theme.palette.dangerColor1 + } + } + ] +} \ No newline at end of file diff --git a/ui/main.qml b/ui/main.qml index 5affe99422d..5be524786eb 100644 --- a/ui/main.qml +++ b/ui/main.qml @@ -452,16 +452,13 @@ StatusWindow { onboardingStore: onboardingStoreLoader.item onFinished: (flow, data) => { - console.warn("!!! ONBOARDING FINISHED; flow:", flow, "; data:", JSON.stringify(data)) - let error = onboardingStoreLoader.item.finishOnboardingFlow(flow, data) if (error != "") { - console.error("!!! ONBOARDING FINISHED WITH ERROR:", error) - // TODO show error + // We should never end up here since we do all validations in the onboarding flow + console.error("onbaording finished with error", error) return } - console.warn("!!! Onboarding completed!") stack.clear() stack.push(splashScreenV2, { runningProgressAnimation: true }) } diff --git a/vendor/status-go b/vendor/status-go index e446e61ab5c..4ed5c9a4de1 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit e446e61ab5c2fa7fbc7fb19fa42c5813b0eea414 +Subproject commit 4ed5c9a4de1a2ccc326b9407230196559fe31091