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
|
visible: true
|
||||||
modal: false
|
modal: false
|
||||||
closePolicy: Popup.NoAutoClose
|
|
||||||
dappUrl: "https://example.com"
|
dappUrl: "https://example.com"
|
||||||
dappIcon: "https://picsum.photos/200/200"
|
dappIcon: "https://picsum.photos/200/200"
|
||||||
dappName: "OpenSea"
|
dappName: "OpenSea"
|
||||||
|
@ -54,8 +53,12 @@ SplitView {
|
||||||
requestPayload: controls.contentToSign[contentToSignComboBox.currentIndex]
|
requestPayload: controls.contentToSign[contentToSignComboBox.currentIndex]
|
||||||
signingTransaction: signingTransaction.checked
|
signingTransaction: signingTransaction.checked
|
||||||
|
|
||||||
|
expirationSeconds: !!ctrlExpiration.text && parseInt(ctrlExpiration.text) ? parseInt(ctrlExpiration.text) : 0
|
||||||
|
onExpirationSecondsChanged: requestTimestamp = new Date()
|
||||||
|
|
||||||
onAccepted: print ("Accepted")
|
onAccepted: print ("Accepted")
|
||||||
onRejected: print ("Rejected")
|
onRejected: print ("Rejected")
|
||||||
|
onClosed: print("Closed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Pane {
|
Pane {
|
||||||
|
@ -142,6 +145,10 @@ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Fusce nibh. Etiam quis
|
||||||
text: "Signing transaction"
|
text: "Signing transaction"
|
||||||
checked: false
|
checked: false
|
||||||
}
|
}
|
||||||
|
TextField {
|
||||||
|
id: ctrlExpiration
|
||||||
|
placeholderText: "Expiration in seconds"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,6 @@ SplitView {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
destroyOnClose: true
|
destroyOnClose: true
|
||||||
modal: false
|
modal: false
|
||||||
closePolicy: Popup.NoAutoClose
|
|
||||||
|
|
||||||
formatBigNumber: (number, symbol, noSymbolOption) => parseFloat(number).toLocaleString(Qt.locale(), 'f', 2)
|
formatBigNumber: (number, symbol, noSymbolOption) => parseFloat(number).toLocaleString(Qt.locale(), 'f', 2)
|
||||||
+ (noSymbolOption ? "" : " " + (symbol || Qt.locale().currencySymbol(Locale.CurrencyIsoCode)))
|
+ (noSymbolOption ? "" : " " + (symbol || Qt.locale().currencySymbol(Locale.CurrencyIsoCode)))
|
||||||
|
@ -106,6 +105,13 @@ SplitView {
|
||||||
loginType: ctrlLoginType.currentIndex
|
loginType: ctrlLoginType.currentIndex
|
||||||
|
|
||||||
feesLoading: ctrlLoading.checked
|
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
|
id: ctrlLoginType
|
||||||
model: Constants.authenticationIconByType
|
model: Constants.authenticationIconByType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: ctrlExpiration
|
||||||
|
placeholderText: "Expiration in seconds"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
onPaint: {
|
onPaint: {
|
||||||
var ctx = getContext("2d")
|
const ctx = getContext("2d")
|
||||||
|
|
||||||
var x = root.width/2
|
var x = root.width/2
|
||||||
var y = root.height/2
|
var y = root.height/2
|
||||||
|
|
|
@ -38,6 +38,9 @@ StatusDialog {
|
||||||
property bool feesLoading
|
property bool feesLoading
|
||||||
property bool signButtonEnabled: true
|
property bool signButtonEnabled: true
|
||||||
|
|
||||||
|
property date requestTimestamp: new Date()
|
||||||
|
property int expirationSeconds
|
||||||
|
|
||||||
property ObjectModel leftFooterContents
|
property ObjectModel leftFooterContents
|
||||||
property ObjectModel rightFooterContents: ObjectModel {
|
property ObjectModel rightFooterContents: ObjectModel {
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
@ -46,6 +49,7 @@ StatusDialog {
|
||||||
StatusFlatButton {
|
StatusFlatButton {
|
||||||
objectName: "rejectButton"
|
objectName: "rejectButton"
|
||||||
Layout.preferredHeight: signButton.height
|
Layout.preferredHeight: signButton.height
|
||||||
|
visible: !countdownPill.isExpired
|
||||||
text: qsTr("Reject")
|
text: qsTr("Reject")
|
||||||
onClicked: root.reject() // close and emit rejected() signal
|
onClicked: root.reject() // close and emit rejected() signal
|
||||||
}
|
}
|
||||||
|
@ -53,11 +57,19 @@ StatusDialog {
|
||||||
objectName: "signButton"
|
objectName: "signButton"
|
||||||
id: signButton
|
id: signButton
|
||||||
interactive: !root.feesLoading && root.signButtonEnabled
|
interactive: !root.feesLoading && root.signButtonEnabled
|
||||||
|
visible: !countdownPill.isExpired
|
||||||
icon.name: Constants.authenticationIconByType[root.loginType]
|
icon.name: Constants.authenticationIconByType[root.loginType]
|
||||||
disabledColor: Theme.palette.directColor8
|
disabledColor: Theme.palette.directColor8
|
||||||
text: qsTr("Sign")
|
text: qsTr("Sign")
|
||||||
onClicked: root.accept() // close and emit accepted() signal
|
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
|
width: 480
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
|
closePolicy: Popup.NoAutoClose
|
||||||
|
|
||||||
function openLinkWithConfirmation(linkUrl) {
|
function openLinkWithConfirmation(linkUrl) {
|
||||||
Global.openLinkWithConfirmation(linkUrl, SQUtils.StringUtils.extractDomainFromLink(linkUrl))
|
Global.openLinkWithConfirmation(linkUrl, SQUtils.StringUtils.extractDomainFromLink(linkUrl))
|
||||||
}
|
}
|
||||||
|
@ -85,7 +99,7 @@ StatusDialog {
|
||||||
visible: root.title || root.subtitle
|
visible: root.title || root.subtitle
|
||||||
headline.title: root.title
|
headline.title: root.title
|
||||||
headline.subtitle: root.subtitle
|
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
|
leftComponent: root.headerIconComponent
|
||||||
}
|
}
|
||||||
|
@ -102,7 +116,7 @@ StatusDialog {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
contentWidth: availableWidth
|
contentWidth: availableWidth
|
||||||
topPadding: 0
|
topPadding: 0
|
||||||
bottomPadding: 0
|
bottomPadding: countdownPill.height
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
@ -116,7 +130,7 @@ StatusDialog {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: -parent.anchors.leftMargin - scrollView.leftPadding
|
Layout.leftMargin: -parent.anchors.leftMargin - scrollView.leftPadding
|
||||||
Layout.rightMargin: -parent.anchors.rightMargin - scrollView.rightPadding
|
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 {
|
gradient: Gradient {
|
||||||
GradientStop { position: 0.0; color: root.gradientColor }
|
GradientStop { position: 0.0; color: root.gradientColor }
|
||||||
GradientStop { position: 1.0; color: root.backgroundColor }
|
GradientStop { position: 1.0; color: root.backgroundColor }
|
||||||
|
@ -208,6 +222,16 @@ StatusDialog {
|
||||||
visible: !!root.infoTagText
|
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 {
|
StatusDialogDivider {
|
||||||
|
|
|
@ -54,8 +54,6 @@ IssuePill {
|
||||||
d.ticker = 0
|
d.ticker = 0
|
||||||
d.secsDiff = 0
|
d.secsDiff = 0
|
||||||
|
|
||||||
console.warn("!!! RESET at:", timestamp, "; expires:", d.expirationTimestamp)
|
|
||||||
|
|
||||||
if (d.expirationTimestamp <= new Date()) {
|
if (d.expirationTimestamp <= new Date()) {
|
||||||
console.warn("Expiration time set in past, or expired already on:", d.expirationTimestamp)
|
console.warn("Expiration time set in past, or expired already on:", d.expirationTimestamp)
|
||||||
d.secsDiff = -1
|
d.secsDiff = -1
|
||||||
|
@ -72,17 +70,16 @@ IssuePill {
|
||||||
readonly property real progress: d.secsDiff >= 0 ? d.secsDiff/(d.expirationTimestamp.valueOf() - timestamp.valueOf()) * 1000
|
readonly property real progress: d.secsDiff >= 0 ? d.secsDiff/(d.expirationTimestamp.valueOf() - timestamp.valueOf()) * 1000
|
||||||
: 0
|
: 0
|
||||||
|
|
||||||
property var expirationTimestamp: root.timestamp
|
property date expirationTimestamp: root.timestamp
|
||||||
property int secsDiff
|
property int secsDiff
|
||||||
property int ticker
|
property int ticker
|
||||||
|
|
||||||
function formatSeconds(seconds) {
|
function formatSeconds(seconds) {
|
||||||
const isoString = new Date(seconds * 1000).toISOString()
|
const days = Math.floor(seconds / 86400)
|
||||||
const days = Math.floor(seconds/86400)
|
const hrs = Math.floor(seconds / 3600) % 24
|
||||||
const hrs = parseInt(isoString.substring(11, 13))
|
const mins = Math.floor(seconds / 60) % 60
|
||||||
const mins = parseInt(isoString.substring(14, 16))
|
|
||||||
|
|
||||||
var result = []
|
const result = []
|
||||||
if (days > 0)
|
if (days > 0)
|
||||||
result.push(qsTr("%1d", "x days").arg(days))
|
result.push(qsTr("%1d", "x days").arg(days))
|
||||||
if (hrs > 0)
|
if (hrs > 0)
|
||||||
|
@ -94,7 +91,7 @@ IssuePill {
|
||||||
result.push(qsTr("%1m", "x minutes").arg(mins))
|
result.push(qsTr("%1m", "x minutes").arg(mins))
|
||||||
}
|
}
|
||||||
if (days === 0 && hrs === 0 && mins === 0) {
|
if (days === 0 && hrs === 0 && mins === 0) {
|
||||||
const secs = parseInt(isoString.substring(17, 19))
|
const secs = Math.floor(seconds)
|
||||||
if (secs >= 0)
|
if (secs >= 0)
|
||||||
result.push(qsTr("%n sec(s)", "", secs))
|
result.push(qsTr("%n sec(s)", "", secs))
|
||||||
}
|
}
|
||||||
|
@ -110,7 +107,6 @@ IssuePill {
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
d.ticker++
|
d.ticker++
|
||||||
d.secsDiff = (d.expirationTimestamp.valueOf() - root.timestamp.valueOf() - d.ticker*1000)/1000
|
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
|
if (d.secsDiff < 0) { // we let it run 1 more second to finish the animation
|
||||||
timer.stop()
|
timer.stop()
|
||||||
root.expired()
|
root.expired()
|
||||||
|
|
|
@ -51,8 +51,8 @@ SignTransactionModalBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
gradientColor: Utils.setColorAlpha(root.accountColor, 0.05) // 5% of wallet color
|
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) :
|
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)
|
: qsTr("%1 wants you to sign this message with %2").arg(root.dappName).arg(root.accountName)
|
||||||
|
|
||||||
fromImageSmartIdenticon.asset.name: "filled-account"
|
fromImageSmartIdenticon.asset.name: "filled-account"
|
||||||
fromImageSmartIdenticon.asset.emoji: root.accountEmoji
|
fromImageSmartIdenticon.asset.emoji: root.accountEmoji
|
||||||
|
|
Loading…
Reference in New Issue