From 6a2b3faeb02c2343513e7237870a227c6b699f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tinkl?= Date: Mon, 16 Sep 2024 20:03:02 +0200 Subject: [PATCH] feat: add countdown pill to sign dialogs and make them unclosable - show countdown until which the sign (WalletConnect and Swap) dialogs expire - after expiration, hide the Reject/Sign buttons and display a plain Close button - make the dialogs non-closable by clicking outside or pressing Esc; the user has to explicitely click some of the footer buttons Fixes #16327 Fixes #16314 --- storybook/pages/DAppSignRequestModalPage.qml | 9 +++++- storybook/pages/SwapSignModalPage.qml | 13 +++++++- .../Controls/StatusCircularProgressBar.qml | 2 +- .../popups/SignTransactionModalBase.qml | 30 +++++++++++++++++-- ui/imports/shared/controls/CountdownPill.qml | 16 ++++------ .../walletconnect/DAppSignRequestModal.qml | 4 +-- 6 files changed, 56 insertions(+), 18 deletions(-) diff --git a/storybook/pages/DAppSignRequestModalPage.qml b/storybook/pages/DAppSignRequestModalPage.qml index e481608a3f..3b5532d111 100644 --- a/storybook/pages/DAppSignRequestModalPage.qml +++ b/storybook/pages/DAppSignRequestModalPage.qml @@ -31,7 +31,6 @@ SplitView { visible: true modal: false - closePolicy: Popup.NoAutoClose dappUrl: "https://example.com" dappIcon: "https://picsum.photos/200/200" dappName: "OpenSea" @@ -54,8 +53,12 @@ SplitView { requestPayload: controls.contentToSign[contentToSignComboBox.currentIndex] signingTransaction: signingTransaction.checked + expirationSeconds: !!ctrlExpiration.text && parseInt(ctrlExpiration.text) ? parseInt(ctrlExpiration.text) : 0 + onExpirationSecondsChanged: requestTimestamp = new Date() + onAccepted: print ("Accepted") onRejected: print ("Rejected") + onClosed: print("Closed") } } Pane { @@ -142,6 +145,10 @@ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Fusce nibh. Etiam quis text: "Signing transaction" checked: false } + TextField { + id: ctrlExpiration + placeholderText: "Expiration in seconds" + } } } } diff --git a/storybook/pages/SwapSignModalPage.qml b/storybook/pages/SwapSignModalPage.qml index 5a05bfdfeb..4d5e36c7a3 100644 --- a/storybook/pages/SwapSignModalPage.qml +++ b/storybook/pages/SwapSignModalPage.qml @@ -72,7 +72,6 @@ SplitView { anchors.centerIn: parent destroyOnClose: true modal: false - closePolicy: Popup.NoAutoClose formatBigNumber: (number, symbol, noSymbolOption) => parseFloat(number).toLocaleString(Qt.locale(), 'f', 2) + (noSymbolOption ? "" : " " + (symbol || Qt.locale().currencySymbol(Locale.CurrencyIsoCode))) @@ -106,6 +105,13 @@ SplitView { loginType: ctrlLoginType.currentIndex feesLoading: ctrlLoading.checked + + expirationSeconds: !!ctrlExpiration.text && parseInt(ctrlExpiration.text) ? parseInt(ctrlExpiration.text) : 0 + onExpirationSecondsChanged: requestTimestamp = new Date() + + onAccepted: logs.logEvent("accepted") + onRejected: logs.logEvent("rejected") + onClosed: logs.logEvent("closed") } } } @@ -178,6 +184,11 @@ SplitView { id: ctrlLoginType model: Constants.authenticationIconByType } + + TextField { + id: ctrlExpiration + placeholderText: "Expiration in seconds" + } } } } diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusCircularProgressBar.qml b/ui/StatusQ/src/StatusQ/Controls/StatusCircularProgressBar.qml index b315594d97..a2528a9578 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusCircularProgressBar.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusCircularProgressBar.qml @@ -33,7 +33,7 @@ Item { } onPaint: { - var ctx = getContext("2d") + const ctx = getContext("2d") var x = root.width/2 var y = root.height/2 diff --git a/ui/app/AppLayouts/Wallet/popups/SignTransactionModalBase.qml b/ui/app/AppLayouts/Wallet/popups/SignTransactionModalBase.qml index 5b04c17547..ef323e87a9 100644 --- a/ui/app/AppLayouts/Wallet/popups/SignTransactionModalBase.qml +++ b/ui/app/AppLayouts/Wallet/popups/SignTransactionModalBase.qml @@ -38,6 +38,9 @@ StatusDialog { property bool feesLoading property bool signButtonEnabled: true + property date requestTimestamp: new Date() + property int expirationSeconds + property ObjectModel leftFooterContents property ObjectModel rightFooterContents: ObjectModel { RowLayout { @@ -46,6 +49,7 @@ StatusDialog { StatusFlatButton { objectName: "rejectButton" Layout.preferredHeight: signButton.height + visible: !countdownPill.isExpired text: qsTr("Reject") onClicked: root.reject() // close and emit rejected() signal } @@ -53,11 +57,19 @@ StatusDialog { objectName: "signButton" id: signButton interactive: !root.feesLoading && root.signButtonEnabled + visible: !countdownPill.isExpired icon.name: Constants.authenticationIconByType[root.loginType] disabledColor: Theme.palette.directColor8 text: qsTr("Sign") onClicked: root.accept() // close and emit accepted() signal } + StatusButton { + objectName: "closeButton" + id: closeButton + visible: countdownPill.isExpired + text: qsTr("Close") + onClicked: root.close() + } } } @@ -77,6 +89,8 @@ StatusDialog { width: 480 padding: 0 + closePolicy: Popup.NoAutoClose + function openLinkWithConfirmation(linkUrl) { Global.openLinkWithConfirmation(linkUrl, SQUtils.StringUtils.extractDomainFromLink(linkUrl)) } @@ -85,7 +99,7 @@ StatusDialog { visible: root.title || root.subtitle headline.title: root.title headline.subtitle: root.subtitle - actions.closeButton.onClicked: root.closeHandler() + actions.closeButton.visible: false // Close hidden explicitely until we have persistent notifications in place to reopen this dialog from outside leftComponent: root.headerIconComponent } @@ -102,7 +116,7 @@ StatusDialog { anchors.fill: parent contentWidth: availableWidth topPadding: 0 - bottomPadding: 0 + bottomPadding: countdownPill.height ColumnLayout { anchors.left: parent.left @@ -116,7 +130,7 @@ StatusDialog { Layout.fillWidth: true Layout.leftMargin: -parent.anchors.leftMargin - scrollView.leftPadding Layout.rightMargin: -parent.anchors.rightMargin - scrollView.rightPadding - Layout.preferredHeight: childrenRect.height + 80 // 40 + 40 top/bottomMargin + Layout.preferredHeight: childrenRect.height + 80 - countdownPill.height // 40 + 40 top/bottomMargin gradient: Gradient { GradientStop { position: 0.0; color: root.gradientColor } GradientStop { position: 1.0; color: root.backgroundColor } @@ -208,6 +222,16 @@ StatusDialog { visible: !!root.infoTagText } } + + CountdownPill { + id: countdownPill + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.current.padding + timestamp: root.requestTimestamp + expirationSeconds: root.expirationSeconds + visible: !!expirationSeconds + } } StatusDialogDivider { diff --git a/ui/imports/shared/controls/CountdownPill.qml b/ui/imports/shared/controls/CountdownPill.qml index 16e86eb1ed..bfa7891490 100644 --- a/ui/imports/shared/controls/CountdownPill.qml +++ b/ui/imports/shared/controls/CountdownPill.qml @@ -54,8 +54,6 @@ IssuePill { d.ticker = 0 d.secsDiff = 0 - console.warn("!!! RESET at:", timestamp, "; expires:", d.expirationTimestamp) - if (d.expirationTimestamp <= new Date()) { console.warn("Expiration time set in past, or expired already on:", d.expirationTimestamp) d.secsDiff = -1 @@ -72,17 +70,16 @@ IssuePill { readonly property real progress: d.secsDiff >= 0 ? d.secsDiff/(d.expirationTimestamp.valueOf() - timestamp.valueOf()) * 1000 : 0 - property var expirationTimestamp: root.timestamp + property date expirationTimestamp: root.timestamp property int secsDiff property int ticker function formatSeconds(seconds) { - const isoString = new Date(seconds * 1000).toISOString() - const days = Math.floor(seconds/86400) - const hrs = parseInt(isoString.substring(11, 13)) - const mins = parseInt(isoString.substring(14, 16)) + const days = Math.floor(seconds / 86400) + const hrs = Math.floor(seconds / 3600) % 24 + const mins = Math.floor(seconds / 60) % 60 - var result = [] + const result = [] if (days > 0) result.push(qsTr("%1d", "x days").arg(days)) if (hrs > 0) @@ -94,7 +91,7 @@ IssuePill { result.push(qsTr("%1m", "x minutes").arg(mins)) } if (days === 0 && hrs === 0 && mins === 0) { - const secs = parseInt(isoString.substring(17, 19)) + const secs = Math.floor(seconds) if (secs >= 0) result.push(qsTr("%n sec(s)", "", secs)) } @@ -110,7 +107,6 @@ IssuePill { onTriggered: { d.ticker++ d.secsDiff = (d.expirationTimestamp.valueOf() - root.timestamp.valueOf() - d.ticker*1000)/1000 - console.warn("!!! REMAINING SECS:", d.secsDiff, "; PROGRESS:", d.progress) if (d.secsDiff < 0) { // we let it run 1 more second to finish the animation timer.stop() root.expired() diff --git a/ui/imports/shared/popups/walletconnect/DAppSignRequestModal.qml b/ui/imports/shared/popups/walletconnect/DAppSignRequestModal.qml index fa2794c872..d898d481ce 100644 --- a/ui/imports/shared/popups/walletconnect/DAppSignRequestModal.qml +++ b/ui/imports/shared/popups/walletconnect/DAppSignRequestModal.qml @@ -51,8 +51,8 @@ SignTransactionModalBase { } gradientColor: Utils.setColorAlpha(root.accountColor, 0.05) // 5% of wallet color - headerMainText: root.signingTransaction ? qsTr("%1 wants you to sign this transaction with %2").arg(root.dappName).arg(root.accountName) : - qsTr("%1 wants you to sign this message with %2").arg(root.dappName).arg(root.accountName) + headerMainText: root.signingTransaction ? qsTr("%1 wants you to sign this transaction with %2").arg(root.dappName).arg(root.accountName) + : qsTr("%1 wants you to sign this message with %2").arg(root.dappName).arg(root.accountName) fromImageSmartIdenticon.asset.name: "filled-account" fromImageSmartIdenticon.asset.emoji: root.accountEmoji