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
This commit is contained in:
Lukáš Tinkl 2024-09-16 20:03:02 +02:00 committed by Lukáš Tinkl
parent 2739d2cf68
commit 6a2b3faeb0
6 changed files with 56 additions and 18 deletions

View File

@ -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"
}
}
}
}

View File

@ -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"
}
}
}
}

View File

@ -33,7 +33,7 @@ Item {
}
onPaint: {
var ctx = getContext("2d")
const ctx = getContext("2d")
var x = root.width/2
var y = root.height/2

View File

@ -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 {

View File

@ -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("%nsec(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()

View File

@ -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