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:
parent
2739d2cf68
commit
6a2b3faeb0
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ Item {
|
|||
}
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
const ctx = getContext("2d")
|
||||
|
||||
var x = root.width/2
|
||||
var y = root.height/2
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue