From 5712df2d1b91d345486c3ba0e2c8f73f04436a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tinkl?= Date: Mon, 14 Jul 2025 13:44:42 +0200 Subject: [PATCH] chore(AppMain): unify the global banner - extract to a separate component, based on simple properties and signals - fix some layout issues in the underlying ModuleWarning component - removed a duplicate seedphrase banner from the wallet section itself - add to storybook, covered with QML tests Fixes #18339 --- .../main/wallet_section/controller.nim | 3 - .../modules/main/wallet_section/module.nim | 2 - src/app/modules/main/wallet_section/view.nim | 10 - storybook/pages/GlobalBannerPage.qml | 102 ++++++++++ storybook/qmlTests/tests/tst_GlobalBanner.qml | 123 ++++++++++++ .../Profile/stores/PrivacyStore.qml | 2 +- .../Profile/stores/ProfileStore.qml | 8 +- ui/app/AppLayouts/Wallet/WalletLayout.qml | 10 +- .../Wallet/panels/SeedPhraseBackupWarning.qml | 88 --------- ui/app/AppLayouts/Wallet/stores/RootStore.qml | 1 - ui/app/mainui/AppMain.qml | 183 ++---------------- ui/app/mainui/GlobalBanner.qml | 99 ++++++++++ ui/app/mainui/qmldir | 1 + ui/i18n/qml_base.ts | 13 +- ui/imports/shared/panels/ModuleWarning.qml | 128 ++++++------ 15 files changed, 410 insertions(+), 363 deletions(-) create mode 100644 storybook/pages/GlobalBannerPage.qml create mode 100644 storybook/qmlTests/tests/tst_GlobalBanner.qml delete mode 100644 ui/app/AppLayouts/Wallet/panels/SeedPhraseBackupWarning.qml create mode 100644 ui/app/mainui/GlobalBanner.qml diff --git a/src/app/modules/main/wallet_section/controller.nim b/src/app/modules/main/wallet_section/controller.nim index e78dfd58bc..da812038d4 100644 --- a/src/app/modules/main/wallet_section/controller.nim +++ b/src/app/modules/main/wallet_section/controller.nim @@ -39,9 +39,6 @@ proc init*(self: Controller) = proc getCurrency*(self: Controller): string = return self.settingsService.getCurrency() -proc isMnemonicBackedUp*(self: Controller): bool = - return self.settingsService.getMnemonic().len > 0 - proc getTotalCurrencyBalance*(self: Controller, addresses: seq[string], chainIds: seq[int]): CurrencyAmount = return currencyAmountToItem(self.walletAccountService.getTotalCurrencyBalance(addresses, chainIds), self.currencyService.getCurrencyFormat(self.getCurrency())) diff --git a/src/app/modules/main/wallet_section/module.nim b/src/app/modules/main/wallet_section/module.nim index e591261d2c..d277aebe7d 100644 --- a/src/app/modules/main/wallet_section/module.nim +++ b/src/app/modules/main/wallet_section/module.nim @@ -399,8 +399,6 @@ proc checkIfModuleDidLoad(self: Module) = if(not self.networksModule.isLoaded()): return - let mnemonicBackedUp = self.controller.isMnemonicBackedUp() - self.view.setData(mnemonicBackedUp) self.setTotalCurrencyBalance() self.filter.setAddresses(self.getWalletAddressesNotHidden()) self.filter.load() diff --git a/src/app/modules/main/wallet_section/view.nim b/src/app/modules/main/wallet_section/view.nim index 197e51859d..4cf2f91f45 100644 --- a/src/app/modules/main/wallet_section/view.nim +++ b/src/app/modules/main/wallet_section/view.nim @@ -15,7 +15,6 @@ QtObject: View* = ref object of QObject delegate: io_interface.AccessInterface totalCurrencyBalance: CurrencyAmount - isMnemonicBackedUp: bool tmpAmount: float # shouldn't be used anywhere except in prepare*/getPrepared* procs tmpSymbol: string # shouldn't be used anywhere except in prepare*/getPrepared* procs activityController: activityc.Controller @@ -79,12 +78,6 @@ QtObject: read = getTotalCurrencyBalance notify = totalCurrencyBalanceChanged - proc getIsMnemonicBackedUp(self: View): QVariant {.slot.} = - return newQVariant(self.isMnemonicBackedUp) - - QtProperty[QVariant] isMnemonicBackedUp: - read = getIsMnemonicBackedUp - proc addressFiltersChanged*(self: View) {.signal.} proc setAddressFilters*(self: View, address: string) = self.addressFilters = address @@ -122,9 +115,6 @@ QtObject: self.tmpSymbol = "ERROR" return newQVariant(currencyAmount) - proc setData*(self: View, mnemonicBackedUp: bool) = - self.isMnemonicBackedUp = mnemonicBackedUp - proc runAddAccountPopup*(self: View, addingWatchOnlyAccount: bool) {.slot.} = self.delegate.runAddAccountPopup(addingWatchOnlyAccount) diff --git a/storybook/pages/GlobalBannerPage.qml b/storybook/pages/GlobalBannerPage.qml new file mode 100644 index 0000000000..e023122e1a --- /dev/null +++ b/storybook/pages/GlobalBannerPage.qml @@ -0,0 +1,102 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Storybook + +import utils +import mainui + +SplitView { + Logs { id: logs } + + orientation: Qt.Vertical + SplitView.fillWidth: true + + QtObject { + id: d + property bool userDeclinedBackupBanner + property bool testnetEnabled + } + + Item { + SplitView.fillWidth: true + SplitView.fillHeight: true + + ColumnLayout { + width: ctrlWidth.value + anchors.centerIn: parent + + Label { + text: "Online > Testnet > Seedphrase" + } + Label { + text: "Width: %1; visible: %2".arg(parent.width).arg(banner.visible ? "true" : "false") + } + + GlobalBanner { + id: banner + Tracer { border.color: "blue" } + + Layout.fillWidth: true + isOnline: ctrlIsOnline.checked + testnetEnabled: d.testnetEnabled + seedphraseBackedUp: d.userDeclinedBackupBanner + + onOpenTestnetPopupRequested: { + logs.logEvent("onOpenTestnetPopupRequested") + d.testnetEnabled = false + } + onOpenBackUpSeedPopupRequested: { + logs.logEvent("onOpenBackUpSeedPopupRequested") + } + onUserDeclinedBackupBannerRequested: { + logs.logEvent("onUserDeclinedBackupBannerRequested") + d.userDeclinedBackupBanner = true + } + } + } + } + + LogsAndControlsPanel { + SplitView.minimumHeight: 150 + SplitView.preferredHeight: 150 + + logsView.logText: logs.logText + + RowLayout { + Switch { + id: ctrlIsOnline + text: "Online" + checked: true + } + + Switch { + id: ctrlTestnetEnabled + text: "Testnet" + checked: d.testnetEnabled + onToggled: d.testnetEnabled = checked + } + + Switch { + id: ctrlUserDeclinedBackupBanner + text: "Seedphrase backed up" + checked: d.userDeclinedBackupBanner + onToggled: d.userDeclinedBackupBanner = checked + } + + ToolSeparator {} + + Label { text: "Width:" } + Slider { + id: ctrlWidth + from: 100 + to: 1000 + value: 600 + } + } + } +} + +// category: Components +// status: good diff --git a/storybook/qmlTests/tests/tst_GlobalBanner.qml b/storybook/qmlTests/tests/tst_GlobalBanner.qml new file mode 100644 index 0000000000..4afa7f13d1 --- /dev/null +++ b/storybook/qmlTests/tests/tst_GlobalBanner.qml @@ -0,0 +1,123 @@ +import QtQuick +import QtTest + +import mainui + +Item { + id: root + width: 800 + height: 600 + + Component { + id: componentUnderTest + GlobalBanner { + anchors.centerIn: parent + width: 600 + isOnline: false + testnetEnabled: false + seedphraseBackedUp: false + } + } + + SignalSpy { + id: testNetButtonSignalSpy + target: controlUnderTest + signalName: "openTestnetPopupRequested" + } + + SignalSpy { + id: seedphraseButtonSignalSpy + target: controlUnderTest + signalName: "openBackUpSeedPopupRequested" + } + + SignalSpy { + id: seedphraseCloseButtonSignalSpy + target: controlUnderTest + signalName: "userDeclinedBackupBannerRequested" + } + + property GlobalBanner controlUnderTest: null + + TestCase { + name: "GlobalBanner" + when: windowShown + + function init() { + testNetButtonSignalSpy.clear() + seedphraseButtonSignalSpy.clear() + seedphraseCloseButtonSignalSpy.clear() + } + + function test_basicGeometry() { + controlUnderTest = createTemporaryObject(componentUnderTest, root) + verify(!!controlUnderTest) + tryVerify(() => controlUnderTest.width > 0) + tryVerify(() => controlUnderTest.height > 0) + } + + // assert that at most 1 banner is displayed at any given time, and it's the correct one + // "1. Offline/online > 2. Testnet > 3. Seedphrase" + function test_variations_data() { + return [ + // offline/online banner + {isOnline: false, testnetEnabled: false, seedphraseBackedUp: false, banner: "connectionInfoBanner"}, + {isOnline: false, testnetEnabled: true, seedphraseBackedUp: false, banner: "connectionInfoBanner"}, + {isOnline: false, testnetEnabled: false, seedphraseBackedUp: true, banner: "connectionInfoBanner"}, + {isOnline: false, testnetEnabled: true, seedphraseBackedUp: true, banner: "connectionInfoBanner"}, + + // testnet banner + {isOnline: true, testnetEnabled: true, seedphraseBackedUp: false, banner: "testnetBanner"}, + {isOnline: true, testnetEnabled: true, seedphraseBackedUp: true, banner: "testnetBanner"}, + + // seedphrase banner + {isOnline: true, testnetEnabled: false, seedphraseBackedUp: false, banner: "secureYourSeedPhraseBanner"}, + + // no banner + {isOnline: true, testnetEnabled: false, seedphraseBackedUp: true, banner: ""}, + ] + } + + function test_variations(data) { + controlUnderTest = createTemporaryObject(componentUnderTest, root, data) + verify(!!controlUnderTest) + if (!!data.banner) { + tryCompare(controlUnderTest.item, "objectName", data.banner) + tryCompare(controlUnderTest.item, "visible", true) + } else { + tryVerify(() => controlUnderTest.item === null) // no banner, not visible at all + } + } + + // test that the testnet banner's button emits the right signal to open the resp. popup + function test_testnet_button() { + controlUnderTest = createTemporaryObject(componentUnderTest, root, + {isOnline: true, testnetEnabled: true, seedphraseBackedUp: false}) + verify(!!controlUnderTest) + tryCompare(controlUnderTest.item, "objectName", "testnetBanner") + + const btn = findChild(controlUnderTest.item, "actionButton") + verify(!!btn) + mouseClick(btn) + compare(testNetButtonSignalSpy.count, 1) + } + + // test that the seedphrase banner's buttons emit the right signals to open the resp. popup + function test_seedphrase_buttons() { + controlUnderTest = createTemporaryObject(componentUnderTest, root, + {isOnline: true, testnetEnabled: false, seedphraseBackedUp: false}) + verify(!!controlUnderTest) + tryCompare(controlUnderTest.item, "objectName", "secureYourSeedPhraseBanner") + + const btn = findChild(controlUnderTest.item, "actionButton") + verify(!!btn) + mouseClick(btn) + compare(seedphraseButtonSignalSpy.count, 1) + + const closeBtn = findChild(controlUnderTest.item, "closeButton") + verify(!!closeBtn) + mouseClick(closeBtn) + compare(seedphraseCloseButtonSignalSpy.count, 1) + } + } +} diff --git a/ui/app/AppLayouts/Profile/stores/PrivacyStore.qml b/ui/app/AppLayouts/Profile/stores/PrivacyStore.qml index e7f7193f58..aa7ec0574e 100644 --- a/ui/app/AppLayouts/Profile/stores/PrivacyStore.qml +++ b/ui/app/AppLayouts/Profile/stores/PrivacyStore.qml @@ -7,7 +7,7 @@ QtObject { property var privacyModule // Module Properties - property bool mnemonicBackedUp: privacyModule.mnemonicBackedUp + readonly property bool mnemonicBackedUp: privacyModule.mnemonicBackedUp readonly property string keyUid: userProfile.keyUid // The following properties wrap Privacy and Security View related properties: diff --git a/ui/app/AppLayouts/Profile/stores/ProfileStore.qml b/ui/app/AppLayouts/Profile/stores/ProfileStore.qml index 2c66d9aa00..1287d860b1 100644 --- a/ui/app/AppLayouts/Profile/stores/ProfileStore.qml +++ b/ui/app/AppLayouts/Profile/stores/ProfileStore.qml @@ -24,7 +24,7 @@ QtObject { property string preferredName: userProfile.preferredName property string profileLargeImage: userProfile.largeImage property string icon: userProfile.icon - property bool userDeclinedBackupBanner: Global.appIsReady? localAccountSensitiveSettings.userDeclinedBackupBanner : false + readonly property bool userDeclinedBackupBanner: Global.appIsReady? localAccountSensitiveSettings.userDeclinedBackupBanner : false readonly property string keyUid: userProfile.keyUid readonly property bool isKeycardUser: userProfile.isKeycardUser readonly property int currentUserStatus: userProfile.currentUserStatus @@ -143,9 +143,7 @@ QtObject { root.profileModule.setIsFirstShowcaseInteraction() } - onUserDeclinedBackupBannerChanged: { - if (userDeclinedBackupBanner !== localAccountSensitiveSettings.userDeclinedBackupBanner) { - localAccountSensitiveSettings.userDeclinedBackupBanner = userDeclinedBackupBanner - } + function setUserDeclinedBackupBanner(value = true) { + localAccountSensitiveSettings.userDeclinedBackupBanner = value } } diff --git a/ui/app/AppLayouts/Wallet/WalletLayout.qml b/ui/app/AppLayouts/Wallet/WalletLayout.qml index 4205b177db..ba0d747cb4 100644 --- a/ui/app/AppLayouts/Wallet/WalletLayout.qml +++ b/ui/app/AppLayouts/Wallet/WalletLayout.qml @@ -213,12 +213,6 @@ Item { } } - SeedPhraseBackupWarning { - id: seedPhraseWarning - width: parent.width - anchors.top: parent.top - } - Component { id: cmpSavedAddresses SavedAddressesView { @@ -281,9 +275,7 @@ Item { StatusSectionLayout { id: walletSectionLayout navBar: root.navBar - anchors.top: seedPhraseWarning.bottom - height: root.height - seedPhraseWarning.height - width: root.width + anchors.fill: parent backButtonName: RootStore.backButtonName notificationCount: activityCenterStore.unreadNotificationsCount hasUnseenNotifications: activityCenterStore.hasUnseenNotifications diff --git a/ui/app/AppLayouts/Wallet/panels/SeedPhraseBackupWarning.qml b/ui/app/AppLayouts/Wallet/panels/SeedPhraseBackupWarning.qml deleted file mode 100644 index cb18569f17..0000000000 --- a/ui/app/AppLayouts/Wallet/panels/SeedPhraseBackupWarning.qml +++ /dev/null @@ -1,88 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects - -import StatusQ.Core -import StatusQ.Core.Theme - -import utils -import shared -import shared.panels -import "../stores" - -Rectangle { - id: root - height: visible ? 32 : 0 - visible: !RootStore.mnemonicBackedUp - color: Theme.palette.dangerColor1 - - Row { - spacing: Theme.halfPadding - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: qsTr("Back up your recovery phrase") - font.pixelSize: Theme.additionalTextSize - anchors.verticalCenter: parent.verticalCenter - color: Theme.palette.white - } - - Button { - width: 58 - height: 24 - contentItem: Item { - anchors.fill: parent - Text { - text: qsTr("Back up") - font.pixelSize: Theme.additionalTextSize - font.weight: Font.Medium - font.family: Theme.baseFont.name - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - color: Theme.palette.white - } - } - background: Rectangle { - radius: 4 - anchors.fill: parent - border.color: Theme.palette.white - color: "#19FFFFFF" - } - StatusMouseArea { - cursorShape: Qt.PointingHandCursor - anchors.fill: parent - onClicked: Global.openBackUpSeedPopup() - } - } - } - - SVGImage { - id: closeImg - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: 18 - source: Theme.svg("close-white") - height: 20 - width: 20 - } - ColorOverlay { - anchors.fill: closeImg - source: closeImg - color: Theme.palette.white - opacity: 0.7 - } - StatusMouseArea { - anchors.fill: closeImg - cursorShape: Qt.PointingHandCursor - onClicked: ParallelAnimation { - PropertyAnimation { target: root; property: "visible"; to: false; } - PropertyAnimation { target: root; property: "y"; to: -1 * root.height } - } - } -} diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml index 7b5c7d9fbe..7312b5fb3f 100644 --- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml @@ -35,7 +35,6 @@ QtObject { /* This property holds address of currently selected account in Wallet Main layout */ readonly property var addressFilters: walletSectionInst.addressFilters readonly property var keypairImportModule: walletSectionInst.keypairImportModule - readonly property string mnemonicBackedUp: walletSectionInst.isMnemonicBackedUp readonly property var transactionActivityStatus: walletSectionInst.activityController.status diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 5e41c22729..78bc5ad4a4 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -3,7 +3,6 @@ import QtQml import QtQuick import QtQuick.Controls -import QtQuick.Window import QtQuick.Layouts import QtQml.Models @@ -827,6 +826,8 @@ Item { property var activityCenterPopupObj: null + readonly property int activeSectionType: appMain.rootStore.activeSectionType + function openActivityCenterPopup() { if (!activityCenterPopupObj) { activityCenterPopupObj = activityCenterPopupComponent.createObject(appMain) @@ -1449,78 +1450,31 @@ Item { id: bannersLayout enabled: !localAppSettings.testEnvironment - && appMain.rootStore.activeSectionType !== Constants.appSection.homePage + && d.activeSectionType !== Constants.appSection.homePage && !SQUtils.Utils.isMobile // Temp disable until we have proper way to handle banners on mobile visible: enabled - property var updateBanner: null - property var connectedBanner: null - readonly property bool isConnected: appMain.rootStore.isOnline - - function processUpdateAvailable() { - if (!updateBanner) - updateBanner = updateBannerComponent.createObject(this) - } - - function processConnected() { - if (!connectedBanner) - connectedBanner = connectedBannerComponent.createObject(this) - } - Layout.fillWidth: true + + // apply left/right margins when we remove the window titlebar + Layout.leftMargin: Qt.platform.os === SQUtils.Utils.mac ? appMain.Window.SafeArea.margins.left : 0 + Layout.rightMargin: Qt.platform.os === SQUtils.Utils.mac ? appMain.Window.SafeArea.margins.right : 0 + Layout.maximumHeight: implicitHeight spacing: 1 - onIsConnectedChanged: { - processConnected() - } - - Component.onCompleted: { - if (!isConnected) - processConnected() - } - - Connections { - target: rootStore.aboutModuleInst - function onAppVersionFetched(available: bool, version: string, url: string) { - rootStore.setLatestVersionInfo(available, version, url); - // TODO when we re-implement check for updates, uncomment this - // bannersLayout.processUpdateAvailable() - } - } - - ModuleWarning { - id: testnetBanner - objectName: "testnetBanner" + GlobalBanner { Layout.fillWidth: true - text: qsTr("Testnet mode enabled. All balances, transactions and dApp interactions will be on testnets.") - buttonText: qsTr("Turn off") - type: ModuleWarning.Warning - iconName: "warning" - active: appMain.networksStore.areTestNetworksEnabled - delay: false - onClicked: Global.openTestnetPopup() - closeBtnVisible: false + + isOnline: appMain.rootStore.isOnline + testnetEnabled: appMain.networksStore.areTestNetworksEnabled + seedphraseBackedUp: appMain.privacyStore.mnemonicBackedUp || appMain.profileStore.userDeclinedBackupBanner + + onOpenTestnetPopupRequested: Global.openTestnetPopup() + onOpenBackUpSeedPopupRequested: popups.openBackUpSeedPopup() + onUserDeclinedBackupBannerRequested: appMain.profileStore.setUserDeclinedBackupBanner() } - ModuleWarning { - id: secureYourSeedPhrase - objectName: "secureYourSeedPhraseBanner" - Layout.fillWidth: true - active: !appMain.profileStore.userDeclinedBackupBanner - && !appMain.privacyStore.mnemonicBackedUp - type: ModuleWarning.Danger - text: qsTr("Secure your recovery phrase") - buttonText: qsTr("Back up now") - delay: false - onClicked: popups.openBackUpSeedPopup() - - onCloseClicked: { - appMain.profileStore.userDeclinedBackupBanner = true - } - } - - ModuleWarning { Layout.fillWidth: true readonly property int progress: appMain.communitiesStore.discordImportProgress @@ -1576,16 +1530,6 @@ Item { onCloseClicked: hide() } - ModuleWarning { - id: downloadingArchivesBanner - Layout.fillWidth: true - active: appMain.communitiesStore.downloadingCommunityHistoryArchives - type: ModuleWarning.Danger - text: qsTr("Downloading message history archives, DO NOT CLOSE THE APP until this banner disappears.") - closeBtnVisible: false - delay: false - } - ModuleWarning { id: mailserverConnectionBanner type: ModuleWarning.Warning @@ -1594,91 +1538,6 @@ Item { Layout.fillWidth: true } - Component { - id: connectedBannerComponent - - ModuleWarning { - id: connectedBanner - property bool isConnected: true - - objectName: "connectionInfoBanner" - Layout.fillWidth: true - text: isConnected ? qsTr("You are back online") : qsTr("Internet connection lost. Reconnect to ensure everything is up to date.") - type: isConnected ? ModuleWarning.Success : ModuleWarning.Danger - - function updateState() { - if (isConnected) - showFor() - else - show(); - } - - Component.onCompleted: { - connectedBanner.isConnected = Qt.binding(() => bannersLayout.isConnected); - } - onIsConnectedChanged: { - updateState(); - } - onCloseClicked: { - hide(); - } - onHideFinished: { - destroy() - bannersLayout.connectedBanner = null - } - } - } - - Component { - id: updateBannerComponent - - ModuleWarning { - readonly property string version: appMain.rootStore.latestVersion - readonly property bool updateAvailable: appMain.rootStore.newVersionAvailable - - objectName: "appVersionUpdateBanner" - Layout.fillWidth: true - type: ModuleWarning.Success - delay: false - text: updateAvailable ? qsTr("A new version of Status (%1) is available").arg(version) - : qsTr("Your version is up to date") - - buttonText: updateAvailable ? qsTr("Update") - : qsTr("Close") - - function updateState() { - if (updateAvailable) - show() - else - showFor(5000) - } - - Component.onCompleted: { - updateState() - } - onUpdateAvailableChanged: { - updateState(); - } - onClicked: { - if (updateAvailable) - Global.openDownloadModal(appMain.rootStore.newVersionAvailable, - appMain.rootStore.latestVersion, - appMain.rootStore.downloadURL) - else - close() - } - onCloseClicked: { - if (updateAvailable) - appMain.rootStore.resetLastVersion(); - hide() - } - onHideFinished: { - destroy() - bannersLayout.updateBanner = null - } - } - } - ConnectionWarnings { id: walletBlockchainConnectionBanner objectName: "walletBlockchainConnectionBanner" @@ -1810,8 +1669,7 @@ Item { anchors.fill: parent currentIndex: { - const activeSectionType = appMain.rootStore.activeSectionType - switch (activeSectionType) { + switch (d.activeSectionType) { case Constants.appSection.homePage: return Constants.appViewStackIndex.homePage case Constants.appSection.chat: @@ -1824,7 +1682,7 @@ Item { } } // Should never be here, correct index must be returned from the for loop above - console.error("Wrong section type:", activeSectionType, + console.error("Wrong section type:", d.activeSectionType, "or section id: ", appMain.rootStore.activeSectionId) return Constants.appViewStackIndex.community case Constants.appSection.communitiesPortal: @@ -1843,8 +1701,7 @@ Item { } } onCurrentIndexChanged: { - const sectionType = appMain.rootStore.activeSectionType - if (sectionType !== Constants.appSection.profile && sectionType !== Constants.appSection.wallet) { + if (d.activeSectionType !== Constants.appSection.profile && d.activeSectionType !== Constants.appSection.wallet) { d.maybeDisplayIntroduceYourselfPopup() } } diff --git a/ui/app/mainui/GlobalBanner.qml b/ui/app/mainui/GlobalBanner.qml new file mode 100644 index 0000000000..98df51824a --- /dev/null +++ b/ui/app/mainui/GlobalBanner.qml @@ -0,0 +1,99 @@ +import QtQuick + +import shared.panels + +Loader { + id: root + + required property bool isOnline + required property bool testnetEnabled + required property bool seedphraseBackedUp + + signal openTestnetPopupRequested() + signal openBackUpSeedPopupRequested() + signal userDeclinedBackupBannerRequested() + + sourceComponent: { + if (d.showConnectedBanner) + return connectedBannerComponent + if (testnetEnabled) + return testnetBannerComponent + if (!seedphraseBackedUp) + return secureYourSeedPhraseComponent + } + + visible: !!item + + onLoaded: item.show() + + QtObject { + id: d + + property bool showConnectedBanner: !root.isOnline // initially, we don't want to show the "You are online" banner + } + + Connections { + target: root + function onIsOnlineChanged() { + if (!root.isOnline) { + d.showConnectedBanner = !root.isOnline // show the banner again if we are offline + } + } + } + + Component { + id: connectedBannerComponent + ModuleWarning { + id: connectedBanner + + objectName: "connectionInfoBanner" + text: root.isOnline ? qsTr("You are back online") : qsTr("Internet connection lost. Reconnect to ensure everything is up to date.") + type: root.isOnline ? ModuleWarning.Success : ModuleWarning.Danger + + Connections { + target: root + function onIsOnlineChanged() { + if (root.isOnline) { // keep showing the connectedBanner for 3 more seconds + d.showConnectedBanner = true + connectedBanner.showFor(3000) + } + } + } + + delay: false + onCloseClicked: { + d.showConnectedBanner = true // keep showing the connectedBanner until the hide animation is done + hide() + } + + onHideFinished: d.showConnectedBanner = false // close the banner until the next offline event + } + } + + Component { + id: testnetBannerComponent + ModuleWarning { + objectName: "testnetBanner" + text: qsTr("Testnet mode enabled. All balances, transactions and dApp interactions will be on testnets.") + buttonText: qsTr("Turn off") + type: ModuleWarning.Warning + iconName: "warning" + delay: false + onClicked: root.openTestnetPopupRequested() + closeBtnVisible: false + } + } + + Component { + id: secureYourSeedPhraseComponent + ModuleWarning { + objectName: "secureYourSeedPhraseBanner" + type: ModuleWarning.Type.Danger + text: qsTr("Secure your recovery phrase") + buttonText: qsTr("Back up now") + delay: false + onClicked: root.openBackUpSeedPopupRequested() + onCloseClicked: root.userDeclinedBackupBannerRequested() + } + } +} diff --git a/ui/app/mainui/qmldir b/ui/app/mainui/qmldir index a4b6e9c0e1..da4b99c4a5 100644 --- a/ui/app/mainui/qmldir +++ b/ui/app/mainui/qmldir @@ -1,4 +1,5 @@ AppMain 1.0 AppMain.qml +GlobalBanner 1.0 GlobalBanner.qml SplashScreen 1.0 SplashScreen.qml Popups 1.0 Popups.qml StatusTrayIcon 1.0 StatusTrayIcon.qml diff --git a/ui/i18n/qml_base.ts b/ui/i18n/qml_base.ts index ff496147b6..8f1c6447f8 100644 --- a/ui/i18n/qml_base.ts +++ b/ui/i18n/qml_base.ts @@ -14553,17 +14553,6 @@ to load The next screen contains your recovery phrase.<br/><b>Anyone</b> who sees it can use it to access to your funds. - - SeedPhraseBackupWarning - - Back up your recovery phrase - Back up your recovery phrase - - - Back up - Back up - - SeedphrasePage @@ -18890,4 +18879,4 @@ If a transaction with a lower nonce is pending, higher nonce transactions will r This is not a valid account name - \ No newline at end of file + diff --git a/ui/imports/shared/panels/ModuleWarning.qml b/ui/imports/shared/panels/ModuleWarning.qml index fa87670c6d..8664a5bfd1 100644 --- a/ui/imports/shared/panels/ModuleWarning.qml +++ b/ui/imports/shared/panels/ModuleWarning.qml @@ -132,20 +132,21 @@ Item { Behavior on color { ColorAnimation { - duration: 150 + duration: Theme.AnimationDuration.Fast } } RowLayout { id: layout - spacing: 12 - anchors.centerIn: parent + spacing: Theme.halfPadding + anchors.fill: parent + anchors.leftMargin: Theme.halfPadding + anchors.rightMargin: Theme.halfPadding StatusRoundIcon { Layout.preferredHeight: 16 Layout.preferredWidth: 16 - Layout.rightMargin: -8 visible: !!root.iconName asset.name: root.iconName asset.bgColor: Theme.palette.indirectColor1 @@ -153,27 +154,56 @@ Item { } StatusBaseText { + objectName: "bannerText" + Layout.fillWidth: true + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter text: root.text font.pixelSize: Theme.additionalTextSize font.weight: Font.Medium color: Theme.palette.indirectColor1 linkColor: color - onLinkActivated: root.linkActivated(link) + onLinkActivated: (link) => root.linkActivated(link) HoverHandler { - id: handler1 + cursorShape: hovered && parent.hoveredLink ? Qt.PointingHandCursor : undefined } - StatusMouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - cursorShape: handler1.hovered && parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + + StatusBaseText { + text: qsTr("%1%").arg(progressBar.value) + visible: progressBar.visible + font.pixelSize: Theme.tertiaryTextFontSize + verticalAlignment: Text.AlignVCenter + color: Theme.palette.white + } + + ProgressBar { + id: progressBar + from: 0 + to: 100 + visible: root.progressValue > -1 + value: root.progressValue + background: Rectangle { + implicitWidth: 64 + implicitHeight: 8 + radius: 8 + color: "transparent" + border.width: 1 + border.color: Theme.palette.white + } + contentItem: Rectangle { + width: progressBar.width*progressBar.position + implicitHeight: 8 + radius: 8 + color: Theme.palette.white } } Button { id: button - visible: text != "" + objectName: "actionButton" + visible: !!text focusPolicy: Qt.NoFocus - padding: 5 onClicked: { root.clicked() } @@ -192,68 +222,28 @@ Item { border.color: Theme.palette.indirectColor3 color: Theme.palette.getColor("white", button.hovered ? 0.4 : 0.1) } - StatusMouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton + HoverHandler { cursorShape: Qt.PointingHandCursor } } - } - StatusBaseText { - anchors.verticalCenter: parent.verticalCenter - anchors.right: progressBar.left - anchors.rightMargin: Theme.halfPadding - text: qsTr("%1%").arg(progressBar.value) - visible: progressBar.visible - font.pixelSize: Theme.tertiaryTextFontSize - verticalAlignment: Text.AlignVCenter - color: Theme.palette.white - } + StatusIcon { + id: closeImg + objectName: "closeButton" + height: 20 + width: 20 + icon: "close-circle" + color: Theme.palette.indirectColor1 + opacity: closeButtonMouseArea.containsMouse ? 1 : 0.7 - ProgressBar { - id: progressBar - anchors.verticalCenter: parent.verticalCenter - anchors.right: closeImg.left - anchors.rightMargin: Theme.bigPadding - from: 0 - to: 100 - visible: root.progressValue > -1 - value: root.progressValue - background: Rectangle { - implicitWidth: 64 - implicitHeight: 8 - radius: 8 - color: "transparent" - border.width: 1 - border.color: Theme.palette.white - } - contentItem: Rectangle { - width: progressBar.width*progressBar.position - implicitHeight: 8 - radius: 8 - color: Theme.palette.white - } - } - - StatusIcon { - id: closeImg - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: 18 - height: 20 - width: 20 - icon: "close-circle" - color: Theme.palette.indirectColor1 - opacity: closeButtonMouseArea.containsMouse ? 1 : 0.7 - - StatusMouseArea { - id: closeButtonMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - root.closeClicked() + StatusMouseArea { + id: closeButtonMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.closeClicked() + } } } }