From ca8a0028a8e9bc2b028bcc2601bb246a875b7af6 Mon Sep 17 00:00:00 2001 From: Alex Jbanca <47811206+alexjba@users.noreply.github.com> Date: Fri, 12 Jul 2024 00:00:15 +0300 Subject: [PATCH] feat(WalletConnect): Implement sign request modal (#15520) * feat(WalletConnect): Implement sign request modal 1. Implementing sign request modal based on SignTransactionModalBase 2. Adding storybook page 3. Integrate it in the app 4. Removing DAppRequestModal 5. Update RoundImageWithBadge to preserve aspect ratio between badge and main image * fix(WalletConnect): Remove unneeded properties from WalletConnectService API Removing `selectedAccountAddress` and `loginType`. These properties are now passed through DAppsWorkflow API * fix(WalletConnect): Removing unnecessary changes --- storybook/pages/DAppRequestModalPage.qml | 196 ---------- storybook/pages/DAppSignRequestModalPage.qml | 137 +++++++ storybook/pages/DAppsWorkflowPage.qml | 3 +- .../qmlTests/tests/tst_DAppsWorkflow.qml | 8 +- .../tests/tst_SwapApproveCapModal.qml | 6 +- .../qmlTests/tests/tst_SwapSignModal.qml | 8 +- ui/StatusQ/src/assets.qrc | 2 + ui/StatusQ/src/assets/img/icons/collapse.svg | 8 + ui/StatusQ/src/assets/img/icons/expand.svg | 8 + .../Wallet/panels/DAppsWorkflow.qml | 92 +++-- .../AppLayouts/Wallet/panels/WalletHeader.qml | 2 + .../popups/SignTransactionModalBase.qml | 66 +++- .../services/dapps/DAppsRequestHandler.qml | 7 + .../services/dapps/WalletConnectService.qml | 2 - .../popups/walletconnect/DAppRequestModal.qml | 362 ------------------ .../walletconnect/DAppSignRequestModal.qml | 192 ++++++++++ .../walletconnect/RoundImageWithBadge.qml | 10 +- .../walletconnect/panels/ContentPanel.qml | 78 ++-- ui/imports/shared/popups/walletconnect/qmldir | 2 +- ui/imports/shared/stores/DAppsStore.qml | 1 - 20 files changed, 540 insertions(+), 650 deletions(-) delete mode 100644 storybook/pages/DAppRequestModalPage.qml create mode 100644 storybook/pages/DAppSignRequestModalPage.qml create mode 100644 ui/StatusQ/src/assets/img/icons/collapse.svg create mode 100644 ui/StatusQ/src/assets/img/icons/expand.svg delete mode 100644 ui/imports/shared/popups/walletconnect/DAppRequestModal.qml create mode 100644 ui/imports/shared/popups/walletconnect/DAppSignRequestModal.qml diff --git a/storybook/pages/DAppRequestModalPage.qml b/storybook/pages/DAppRequestModalPage.qml deleted file mode 100644 index d32bb6259e..0000000000 --- a/storybook/pages/DAppRequestModalPage.qml +++ /dev/null @@ -1,196 +0,0 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQml 2.15 -import Qt.labs.settings 1.0 -import QtTest 1.15 - -import StatusQ.Core 0.1 -import StatusQ.Core.Utils 0.1 -import StatusQ.Controls 0.1 -import StatusQ.Components 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Popups.Dialog 0.1 - -import Models 1.0 -import Storybook 1.0 - -import shared.popups.walletconnect 1.0 - -import SortFilterProxyModel 0.2 - -import AppLayouts.Wallet.panels 1.0 -import AppLayouts.Wallet.services.dapps.types 1.0 - -import utils 1.0 -import shared.stores 1.0 - -Item { - id: root - - function openModal() { - modal.open() - } - - // qml Splitter - SplitView { - anchors.fill: parent - - ColumnLayout { - SplitView.fillWidth: true - - Component.onCompleted: root.openModal() - - DAppRequestModal { - id: modal - - anchors.centerIn: parent - - spacing: 8 - - dappName: settings.dappName - dappUrl: settings.dappUrl - dappIcon: settings.dappIcon - payloadData: d.currentPayload ? d.currentPayload.payloadData : null - method: d.currentPayload ? d.currentPayload.method : "" - maxFeesText: d.currentPayload ? d.currentPayload.maxFeesText : "" - maxFeesEthText: d.currentPayload ? d.currentPayload.maxFeesEthText : "" - enoughFunds: settings.enoughFunds - estimatedTimeText: d.currentPayload ? d.currentPayload.estimatedTimeText : "" - - account: d.selectedAccount - network: d.selectedNetwork - - onSign: { - console.log("Sign button clicked") - } - onReject: { - console.log("Reject button clicked") - } - } - - StatusButton { - id: openButton - - Layout.alignment: Qt.AlignHCenter - Layout.margins: 20 - - text: "Open DAppRequestModal" - - onClicked: root.openModal() - } - - ColumnLayout {} - } - - ColumnLayout { - id: optionsSpace - - TextField { - id: dappNameTextField - - text: settings.dappName - onTextChanged: settings.dappName = text - } - TextField { - id: dappUrlTextField - - text: settings.dappUrl - onTextChanged: settings.dappUrl = text - } - TextField { - id: dappIconTextField - - text: settings.dappIcon - onTextChanged: settings.dappIcon = text - } - TextField { - id: accountDisplayTextField - - text: settings.accountDisplay - onTextChanged: settings.accountDisplay = text - } - StatusComboBox { - id: methodsComboBox - - model: d.methodsModel - control.textRole: "method" - currentIndex: settings.payloadMethod - onCurrentIndexChanged: { - d.currentPayload = null - settings.payloadMethod = currentIndex - d.currentPayload = d.payloadOptions[currentIndex] - } - } - StatusCheckBox { - id: enoughFundsCheckBox - - text: "Enough funds" - checked: settings.enoughFunds - onCheckedChanged: settings.enoughFunds = checked - } - - Item { Layout.fillHeight: true } - } - } - - Settings { - id: settings - - property string dappName: "OpenSea" - property string dappUrl: "opensea.io" - property string dappIcon: "https://opensea.io/static/images/logos/opensea-logo.svg" - property string accountDisplay: "helloworld" - property int payloadMethod: 0 - property bool enoughFunds: true - } - - QtObject { - id: d - - Component.onCompleted: methodsModel.append(payloadOptions) - - readonly property var accountsModel: WalletAccountsModel{} - readonly property var selectedAccount: accountsModel.data[0] - - readonly property var selectedNetwork: NetworksModel.flatNetworks.get(0) - - readonly property ListModel methodsModel: ListModel {} - property var currentPayload: payloadOptions[settings.payloadMethod] - property string maxFeesText: "" - property string estimatedTimeText: "" - - readonly property var payloadOptions: [ - { - payloadData: {"message":"This is a message to sign.\nSigning this will prove ownership of the account."}, - method: SessionRequest.methods.personalSign.name, - maxFeesText: "", - maxFeesEthText: "", - estimatedTimeText: "" - }, - { - payloadData: {"message": "{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallet\",\"type\":\"address\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person\"},{\"name\":\"contents\",\"type\":\"string\"}]},\"primaryType\":\"Mail\",\"domain\":{\"name\":\"Ether Mail\",\"version\":\"1\",\"chainId\":1,\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\"},\"message\":{\"from\":{\"name\":\"Cow\",\"wallet\":\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\"},\"to\":{\"name\":\"Bob\",\"wallet\":\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\"},\"contents\":\"Hello, Bob!\"}}"}, - method: SessionRequest.methods.signTypedData_v4.name, - maxFeesText: "", - maxFeesEthText: "", - estimatedTimeText: "" - }, - { - payloadData: {"tx":{"data":"0x","from":"0xE2d622C817878dA5143bBE06866ca8E35273Ba8a","gasLimit":"0x5208","gasPrice":"0x048ddbc5","nonce":"0x2a","to":"0xE2d622C817878dA5143bBE06866ca8E35273Ba8a","value":"0x00"}}, - method: SessionRequest.methods.signTransaction.name, - maxFeesText: "1.82 EUR", - maxFeesEthText: "0.0001 ETH", - estimatedTimeText: "3-5 mins" - }, - { - payloadData: {"tx":{"data":"0x","from":"0xE2d622C817878dA5143bBE06866ca8E35273Ba8a","gasLimit":"0x5208","gasPrice":"0x048ddbc5","nonce":"0x2a","to":"0xE2d622C817878dA5143bBE06866ca8E35273Ba8a","value":"0x00"}}, - method: SessionRequest.methods.sendTransaction.name, - maxFeesText: "0.92 EUR", - maxFeesEthText: "0.00005 ETH", - estimatedTimeText: "1-2 mins" - } - ] - } -} - -// category: Wallet diff --git a/storybook/pages/DAppSignRequestModalPage.qml b/storybook/pages/DAppSignRequestModalPage.qml new file mode 100644 index 0000000000..edf7bb3db4 --- /dev/null +++ b/storybook/pages/DAppSignRequestModalPage.qml @@ -0,0 +1,137 @@ +// category: Popups + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import shared.popups.walletconnect 1.0 +import utils 1.0 +import Storybook 1.0 + +SplitView { + id: root + + PopupBackground { + SplitView.fillWidth: true + SplitView.fillHeight: true + Button { + anchors.centerIn: parent + text: "Open" + onClicked: dappSignRequestModal.visible = true + } + + DAppSignRequestModal { + id: dappSignRequestModal + + loginType: loginType.currentValue + visible: true + modal: false + closePolicy: Popup.NoAutoClose + dappUrl: "https://example.com" + dappIcon: "https://picsum.photos/200/200" + dappName: "OpenSea" + accountColor: "blue" + accountName: "Account Name" + accountAddress: "0xE2d622C817878dA5143bBE06866ca8E35273Ba8" + networkName: "Ethereum" + networkIconPath: "https://picsum.photos/200/200" + + currentCurrency: "EUR" + fiatFees: fiatFees.text + cryptoFees: "0.001" + estimatedTime: "3-5 minutes" + feesLoading: feesLoading.checked + hasFees: hasFees.checked + enoughFundsForTransaction: enoughFeesForTransaction.checked + enoughFundsForFees: enoughFeesForGas.checked + + // sun emoji + accountEmoji: "\u2600" + requestPayload: controls.contentToSign[contentToSignComboBox.currentIndex] + signingTransaction: signingTransaction.checked + + onAccepted: print ("Accepted") + onRejected: print ("Rejected") + } + } + Pane { + id: controls + SplitView.preferredWidth: 300 + SplitView.fillHeight: true + + readonly property var contentToSign: ['{ + "id": 1714038548266495, + "params": { + "chainld": "eip155:11155111", + "request": { + "expiryTimestamp": 1714038848, + "method": "eth_signTransaction", + "params": [{ + "data": "0x", + "from": "0xE2d622C817878dA5143bBE06866ca8E35273Ba8", + "gasLimit": "0x5208", + "gasPrice": "0xa677ef31", + "nonce": "0x27", + "to": "0xE2d622C817878dA5143bBE06866ca8E35273Ba8a", + "value": "0x00" + }] + } + }, + "topic": "a0f85b23a1f3a540d85760a523963165fb92169d57320c", + "verifyContext": { + "verified": { + "isScam": false, + "origin": "https://react-app.walletconnect.com/", + "validation": "VALID", + "verifyUrl": "https://verify.walletconnect.com/" + } + } + }', + '"tx":{"data":"0x","from":"0xE2d622C817878dA5143bBE06866ca8E35273Ba8a","gasLimit":"0x5208","gasPrice":"0x048ddbc5","nonce":"0x2a","to":"0xE2d622C817878dA5143bBE06866ca8E35273Ba8a","value":"0x00"}', + "" + ] + + ColumnLayout { + TextField { + id: fiatFees + text: "1.54" + } + ComboBox { + id: loginType + model: [{name: "Password", value: Constants.LoginType.Password}, {name: "Biometrics", value: Constants.LoginType.Biometrics}, {name: "Keycard", value: Constants.LoginType.Keycard}] + textRole: "name" + valueRole: "value" + currentIndex: 0 + } + ComboBox { + id: contentToSignComboBox + model: ["Long content to sign", "Short content to sign", "Empty content to sign"] + currentIndex: 0 + } + CheckBox { + id: enoughFeesForTransaction + text: "Enough fees for transaction" + checked: true + } + CheckBox { + id: enoughFeesForGas + text: "Enough fees for gas" + checked: true + } + CheckBox { + id: feesLoading + text: "Fees loading" + checked: false + } + CheckBox { + id: hasFees + text: "Has fees" + checked: true + } + CheckBox { + id: signingTransaction + text: "Signing transaction" + checked: false + } + } + } +} diff --git a/storybook/pages/DAppsWorkflowPage.qml b/storybook/pages/DAppsWorkflowPage.qml index f278db1b70..73212670f5 100644 --- a/storybook/pages/DAppsWorkflowPage.qml +++ b/storybook/pages/DAppsWorkflowPage.qml @@ -61,6 +61,8 @@ Item { spacing: 8 wcService: walletConnectService + loginType: Constants.LoginType.Biometrics + selectedAccountAddress: "" } } ColumnLayout {} @@ -373,7 +375,6 @@ Item { } walletRootStore: QObject { - property string selectedAddress: "" property var filteredFlatModel: SortFilterProxyModel { sourceModel: NetworksModel.flatNetworks filters: ValueFilter { roleName: "isTest"; value: settings.testNetworks; } diff --git a/storybook/qmlTests/tests/tst_DAppsWorkflow.qml b/storybook/qmlTests/tests/tst_DAppsWorkflow.qml index d7daed93fc..7c764613af 100644 --- a/storybook/qmlTests/tests/tst_DAppsWorkflow.qml +++ b/storybook/qmlTests/tests/tst_DAppsWorkflow.qml @@ -121,7 +121,6 @@ Item { id: walletStoreComponent QtObject { - property string selectedAddress: "" readonly property ListModel filteredFlatModel: ListModel { ListElement { chainId: 1 } ListElement { @@ -503,6 +502,7 @@ Item { Component { id: componentUnderTest DAppsWorkflow { + loginType: Constants.LoginType.Password } } @@ -605,9 +605,9 @@ Item { verify(popup.visible) compare(popup.dappName, td.session.peer.metadata.name) - compare(popup.account.name, td.account.name) - compare(popup.account.address, td.account.address) - compare(popup.network.chainId, td.network.chainId) + compare(popup.accountName, td.account.name) + compare(popup.accountAddress, td.account.address) + compare(popup.networkName, td.network.chainName) popup.close() waitForRendering(controlUnderTest) diff --git a/storybook/qmlTests/tests/tst_SwapApproveCapModal.qml b/storybook/qmlTests/tests/tst_SwapApproveCapModal.qml index 4b7b308580..cb7b63f39e 100644 --- a/storybook/qmlTests/tests/tst_SwapApproveCapModal.qml +++ b/storybook/qmlTests/tests/tst_SwapApproveCapModal.qml @@ -93,16 +93,16 @@ Item { .arg(controlUnderTest.formatBigNumber(controlUnderTest.fromTokenAmount)).arg(controlUnderTest.fromTokenSymbol) .arg(controlUnderTest.accountName).arg(controlUnderTest.serviceProviderURL).arg(controlUnderTest.networkName)) - const fromImageHidden = findChild(controlUnderTest.contentItem, "fromImage") + const fromImageHidden = findChild(controlUnderTest.contentItem, "fromImageIdenticon") compare(fromImageHidden.visible, false) const fromImage = findChild(controlUnderTest.contentItem, "fromImageIdenticon") verify(!!fromImage) compare(fromImage.asset.emoji, controlUnderTest.accountEmoji) compare(fromImage.asset.color, controlUnderTest.accountColor) - const toImage = findChild(controlUnderTest.contentItem, "toImage") + const toImage = findChild(controlUnderTest.contentItem, "toImageIdenticon") verify(!!toImage) - compare(toImage.image.source, Constants.tokenIcon(controlUnderTest.fromTokenSymbol)) + compare(toImage.asset.name, Constants.tokenIcon(controlUnderTest.fromTokenSymbol)) // spending cap box const spendingCapBox = findChild(controlUnderTest.contentItem, "spendingCapBox") diff --git a/storybook/qmlTests/tests/tst_SwapSignModal.qml b/storybook/qmlTests/tests/tst_SwapSignModal.qml index ab37cae83c..beaa50e282 100644 --- a/storybook/qmlTests/tests/tst_SwapSignModal.qml +++ b/storybook/qmlTests/tests/tst_SwapSignModal.qml @@ -95,12 +95,12 @@ Item { const headerText = findChild(controlUnderTest.contentItem, "headerText") verify(!!headerText) compare(headerText.text, qsTr("Swap 1000.123456789 SNT to 1.42 ETH in %1 on %2").arg(controlUnderTest.accountName).arg(controlUnderTest.networkName)) - const fromImage = findChild(controlUnderTest.contentItem, "fromImage") + const fromImage = findChild(controlUnderTest.contentItem, "fromImageIdenticon") verify(!!fromImage) - compare(fromImage.image.source, Constants.tokenIcon(controlUnderTest.fromTokenSymbol)) - const toImage = findChild(controlUnderTest.contentItem, "toImage") + compare(fromImage.asset.name, Constants.tokenIcon(controlUnderTest.fromTokenSymbol)) + const toImage = findChild(controlUnderTest.contentItem, "toImageIdenticon") verify(!!toImage) - compare(toImage.image.source, Constants.tokenIcon(controlUnderTest.toTokenSymbol)) + compare(toImage.asset.name, Constants.tokenIcon(controlUnderTest.toTokenSymbol)) // pay box const payBox = findChild(controlUnderTest.contentItem, "payBox") diff --git a/ui/StatusQ/src/assets.qrc b/ui/StatusQ/src/assets.qrc index af5b276c17..e6c3245fbf 100644 --- a/ui/StatusQ/src/assets.qrc +++ b/ui/StatusQ/src/assets.qrc @@ -165,6 +165,7 @@ assets/img/icons/checkmark.svg assets/img/icons/chevron-down.svg assets/img/icons/chevron-up.svg + assets/img/icons/collapse.svg assets/img/icons/clear.svg assets/img/icons/close-circle.svg assets/img/icons/close.svg @@ -187,6 +188,7 @@ assets/img/icons/emojis.svg assets/img/icons/ETH.png assets/img/icons/exchange.svg + assets/img/icons/expand.svg assets/img/icons/external.svg assets/img/icons/external-link.svg assets/img/icons/face-id.svg diff --git a/ui/StatusQ/src/assets/img/icons/collapse.svg b/ui/StatusQ/src/assets/img/icons/collapse.svg new file mode 100644 index 0000000000..c764b4aa0e --- /dev/null +++ b/ui/StatusQ/src/assets/img/icons/collapse.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui/StatusQ/src/assets/img/icons/expand.svg b/ui/StatusQ/src/assets/img/icons/expand.svg new file mode 100644 index 0000000000..820a769ffd --- /dev/null +++ b/ui/StatusQ/src/assets/img/icons/expand.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml index a716fa0b59..dd2c6d1201 100644 --- a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml +++ b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml @@ -11,13 +11,15 @@ import shared.popups.walletconnect 1.0 import AppLayouts.Wallet.services.dapps 1.0 import AppLayouts.Wallet.services.dapps.types 1.0 -import shared.stores 1.0 import utils 1.0 DappsComboBox { id: root required property WalletConnectService wcService + // Values mapped to Constants.LoginType + required property int loginType + property string selectedAccountAddress signal pairWCReady() @@ -85,7 +87,7 @@ DappsComboBox { } ] } - selectedAccountAddress: root.wcService.selectedAccountAddress + selectedAccountAddress: root.selectedAccountAddress dAppUrl: proposalMedatada.url dAppName: proposalMedatada.name @@ -116,26 +118,62 @@ DappsComboBox { property SessionRequestResolved request: null - sourceComponent: DAppRequestModal { - account: request.account - network: request.network - - dappName: request.dappName - dappUrl: request.dappUrl - dappIcon: request.dappIcon - - payloadData: request.data - method: request.method - maxFeesText: request.maxFeesText - maxFeesEthText: request.maxFeesEthText - enoughFunds: request.enoughFunds - estimatedTimeText: request.estimatedTimeText - + sourceComponent: DAppSignRequestModal { + objectName: "dappsRequestModal" + loginType: request.account.migragedToKeycard ? Constants.LoginType.Keycard : root.loginType visible: true - onClosed: sessionRequestLoader.active = false + dappUrl: request.dappUrl + dappIcon: request.dappIcon + dappName: request.dappName - onSign: { + accountColor: request.account.color + accountName: request.account.name + accountAddress: request.account.address + accountEmoji: request.account.emoji + + networkName: request.network.chainName + networkIconPath: Style.svg(request.network.iconUrl) + + currentCurrency: "" + fiatFees: request.maxFeesText + cryptoFees: request.maxFeesEthText + estimatedTime: request.estimatedTimeText + feesLoading: !request.maxFeesText || !request.maxFeesEthText + hasFees: signingTransaction + enoughFundsForTransaction: request.enoughFunds + enoughFundsForFees: request.enoughFunds + + signingTransaction: request.method === SessionRequest.methods.signTransaction.name || request.method === SessionRequest.methods.sendTransaction.name + requestPayload: { + switch(request.method) { + case SessionRequest.methods.personalSign.name: + return SessionRequest.methods.personalSign.getMessageFromData(request.data) + case SessionRequest.methods.sign.name: { + return SessionRequest.methods.sign.getMessageFromData(request.data) + } + case SessionRequest.methods.signTypedData_v4.name: { + const stringPayload = SessionRequest.methods.signTypedData_v4.getMessageFromData(request.data) + return JSON.stringify(JSON.parse(stringPayload), null, 2) + } + case SessionRequest.methods.signTypedData.name: { + const stringPayload = SessionRequest.methods.signTypedData.getMessageFromData(root.payloadData) + return JSON.stringify(JSON.parse(stringPayload), null, 2) + } + case SessionRequest.methods.signTransaction.name: { + const jsonPayload = SessionRequest.methods.signTransaction.getTxObjFromData(request.data) + return JSON.stringify(jsonPayload, null, 2) + } + case SessionRequest.methods.sendTransaction.name: { + const jsonPayload = SessionRequest.methods.sendTransaction.getTxObjFromData(request.data) + return JSON.stringify(jsonPayload, null, 2) + } + } + } + + onClosed: Qt.callLater( () => sessionRequestLoader.active = false) + + onAccepted: { if (!request) { console.error("Error signing: request is null") return @@ -143,28 +181,32 @@ DappsComboBox { root.wcService.requestHandler.authenticate(request) } - onReject: { + onRejected: { let userRejected = true root.wcService.requestHandler.rejectSessionRequest(request, userRejected) - close() } Connections { target: root.wcService.requestHandler function onMaxFeesUpdated(maxFees, maxFeesWei, haveEnoughFunds, symbol) { - maxFeesText = `${maxFees.toFixed(2)} ${symbol}` + fiatFees = maxFees + currentCurrency = symbol + var ethStr = "?" try { ethStr = globalUtils.wei2Eth(maxFeesWei, 9) } catch (e) { // ignore error in case of tests and storybook where we don't have access to globalUtils } - maxFeesEthText = `${ethStr} ETH` - enoughFunds = haveEnoughFunds + cryptoFees = ethStr + enoughFundsForTransaction = haveEnoughFunds + enoughFundsForFees = haveEnoughFunds + feesLoading = false + hasFees = !!maxFees } function onEstimatedTimeUpdated(minMinutes, maxMinutes) { - estimatedTimeText = qsTr("%1-%2mins").arg(minMinutes).arg(maxMinutes) + estimatedTime = qsTr("%1-%2mins").arg(minMinutes).arg(maxMinutes) } } } diff --git a/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml b/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml index a9944edca8..c8c8d9ae3a 100644 --- a/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml +++ b/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml @@ -128,6 +128,8 @@ Item { enabled: !!Global.walletConnectService wcService: Global.walletConnectService + loginType: root.store.loginType + selectedAccountAddress: root.walletStore.selectedAddress } StatusButton { diff --git a/ui/app/AppLayouts/Wallet/popups/SignTransactionModalBase.qml b/ui/app/AppLayouts/Wallet/popups/SignTransactionModalBase.qml index f145bd9fb0..c9b88707f3 100644 --- a/ui/app/AppLayouts/Wallet/popups/SignTransactionModalBase.qml +++ b/ui/app/AppLayouts/Wallet/popups/SignTransactionModalBase.qml @@ -2,6 +2,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import QtQml.Models 2.15 +import QtGraphicalEffects 1.15 import StatusQ 0.1 import StatusQ.Core 0.1 @@ -24,6 +25,7 @@ StatusDialog { property Component headerIconComponent property bool feesLoading + property bool signButtonEnabled: true property ObjectModel leftFooterContents property ObjectModel rightFooterContents: ObjectModel { @@ -39,7 +41,7 @@ StatusDialog { StatusButton { objectName: "signButton" id: signButton - interactive: !root.feesLoading + interactive: !root.feesLoading && root.signButtonEnabled icon.name: Constants.authenticationIconByType[root.loginType] text: qsTr("Sign") onClicked: root.accept() // close and emit accepted() signal @@ -51,11 +53,14 @@ StatusDialog { property url fromImageSource property alias fromImageSmartIdenticon: fromImageSmartIdenticon property url toImageSource + readonly property alias toImageSmartIdenticon: toImageSmartIdenticon property alias headerMainText: headerMainText.text - property alias headerSubTextLayout: headerSubTextLayout.children + readonly property alias headerSubTextLayout: headerSubTextLayout.children property string infoTagText + readonly property alias infoTag: infoTag + property bool showHeaderDivider: true - default property alias contents: contentsLayout.children + default property alias contents: contentsLayout.data width: 480 padding: 0 @@ -127,26 +132,48 @@ StatusDialog { id: fromImageSmartIdenticon width: 40 height: 40 + asset.name: root.fromImageSource + asset.width: 40 + asset.height: 40 asset.bgWidth: 40 asset.bgHeight: 40 + asset.color: "transparent" + asset.bgColor: "transparent" visible: !!asset.name + layer.enabled: toImageSmartIdenticon.visible + layer.effect: OpacityMask { + id: mask + invert: true + + maskSource: Item { + width: mask.width + 4 + height: mask.height + 4 + + Rectangle { + anchors.centerIn: parent + anchors.horizontalCenterOffset: toImageSmartIdenticon.width - 10 + + width: parent.width + height: width + radius: width / 2 + } + } + } } - StatusRoundedImage { - objectName: "fromImage" - width: 42 - height: 42 - border.width: 2 - border.color: "transparent" - image.source: root.fromImageSource - visible: root.fromImageSource.toString() !== "" - } - StatusRoundedImage { - objectName: "toImage" - width: 42 - height: 42 - border.width: 2 - border.color: Theme.palette.statusBadge.foregroundColor - image.source: root.toImageSource + + StatusSmartIdenticon { + objectName: "toImageIdenticon" + id: toImageSmartIdenticon + width: 40 + height: 40 + asset.bgWidth: 40 + asset.bgHeight: 40 + visible: !!asset.name || !!asset.source + asset.name: root.toImageSource + asset.width: 40 + asset.height: 40 + asset.color: "transparent" + asset.bgColor: "transparent" } } @@ -182,6 +209,7 @@ StatusDialog { StatusDialogDivider { Layout.fillWidth: true Layout.bottomMargin: Style.current.bigPadding + visible: root.showHeaderDivider } ColumnLayout { diff --git a/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml b/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml index 3be258f1e4..f865c38a2c 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml @@ -159,6 +159,13 @@ QObject { obj.resolveDappInfoFromSession(session) root.sessionRequest(obj) // TODO #15192: update maxFees + if (!event.params.request.params[0].gasLimit || !event.params.request.params[0].gasPrice) { + root.maxFeesUpdated(0, 0, true, "") + root.estimatedTimeUpdated(0, 0) + return + } + + let gasLimit = parseFloat(parseInt(event.params.request.params[0].gasLimit, 16)); let gasPrice = parseFloat(parseInt(event.params.request.params[0].gasPrice, 16)); let maxFees = gasLimit * gasPrice diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml index 488d1a9f09..b607cce9ab 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml @@ -32,8 +32,6 @@ QObject { required property DAppsStore store required property var walletRootStore - readonly property string selectedAccountAddress: walletRootStore.selectedAddress - readonly property alias dappsModel: dappsProvider.dappsModel readonly property alias requestHandler: requestHandler diff --git a/ui/imports/shared/popups/walletconnect/DAppRequestModal.qml b/ui/imports/shared/popups/walletconnect/DAppRequestModal.qml deleted file mode 100644 index fb8b4969bf..0000000000 --- a/ui/imports/shared/popups/walletconnect/DAppRequestModal.qml +++ /dev/null @@ -1,362 +0,0 @@ -import QtQuick 2.15 -import QtQuick.Layouts 1.15 -import QtQml.Models 2.15 - -import StatusQ.Core 0.1 -import StatusQ.Popups.Dialog 0.1 -import StatusQ.Controls 0.1 -import StatusQ.Components 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Core.Utils 0.1 as StatusQ - -import shared.popups.walletconnect.panels 1.0 -import utils 1.0 - -import AppLayouts.Wallet.services.dapps.types 1.0 - -StatusDialog { - id: root - - objectName: "dappsRequestModal" - - implicitWidth: 480 - - required property string dappName - required property string dappUrl - required property url dappIcon - required property string method - required property var payloadData - property string maxFeesText: "" - property string maxFeesEthText: "" - property bool enoughFunds: false - property string estimatedTimeText: "" - - required property var account - property var network: null - - signal sign() - signal reject() - - title: qsTr("Sign request") - - padding: 20 - - onPayloadDataChanged: d.updateDisplay() - onMethodChanged: d.updateDisplay() - Component.onCompleted: d.updateDisplay() - - contentItem: StatusScrollView { - id: scrollView - padding: 0 - ColumnLayout { - spacing: 20 - clip: true - - width: scrollView.availableWidth - - IntentionPanel { - Layout.fillWidth: true - - dappName: root.dappName - dappIcon: root.dappIcon - account: root.account - - userDisplayNaming: d.userDisplayNaming - } - - ContentPanel { - Layout.fillWidth: true - Layout.maximumHeight: 340 - - payloadToDisplay: d.payloadToDisplay - } - - // TODO: externalize as a TargetPanel - ColumnLayout { - spacing: 8 - - StatusBaseText { - text: qsTr("Sign with") - font.pixelSize: 13 - color: Theme.palette.directColor1 - } - - // TODO #14762: implement proper control to display the accounts details - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 76 - - radius: 8 - border.width: 1 - border.color: Theme.palette.baseColor2 - color: "transparent" - - RowLayout { - spacing: 12 - anchors.fill: parent - anchors.margins: 16 - - StatusSmartIdenticon { - width: 40 - height: 40 - - asset: StatusAssetSettings { - color: Theme.palette.primaryColor1 - isImage: false - isLetterIdenticon: true - useAcronymForLetterIdenticon: false - emoji: root.account.emoji - } - } - - ColumnLayout { - Layout.alignment: Qt.AlignLeft - - StatusBaseText { - text: root.account.name - - Layout.alignment: Qt.AlignLeft - - font.pixelSize: 13 - } - StatusBaseText { - text: StatusQ.Utils.elideAndFormatWalletAddress(root.account.address, 6, 4) - - Layout.alignment: Qt.AlignLeft - - font.pixelSize: 13 - - color: Theme.palette.baseColor1 - } - } - - Item {Layout.fillWidth: true } - } - } - - StatusBaseText { - text: qsTr("Network") - font.pixelSize: 13 - color: Theme.palette.directColor1 - } - - // TODO #14762: implement proper control to display the chain - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 76 - - visible: root.network !== null - - radius: 8 - border.width: 1 - border.color: Theme.palette.baseColor2 - color: "transparent" - - RowLayout { - spacing: 12 - anchors.fill: parent - anchors.margins: 16 - - StatusSmartIdenticon { - width: 40 - height: 40 - - asset: StatusAssetSettings { - isImage: true - name: !!root.network ? Style.svg("tiny/" + root.network.iconUrl) : "" - } - } - - StatusBaseText { - text: !!root.network ? root.network.chainName : "" - - Layout.alignment: Qt.AlignLeft - - font.pixelSize: 13 - } - Item {Layout.fillWidth: true } - } - } - - StatusBaseText { - text: qsTr("Fees") - font.pixelSize: 13 - color: Theme.palette.directColor1 - visible: d.isTransaction() - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 76 - - visible: root.network !== null && d.isTransaction() - - radius: 8 - border.width: 1 - border.color: Theme.palette.baseColor2 - color: "transparent" - - RowLayout { - spacing: 12 - anchors.fill: parent - anchors.margins: 16 - - StatusBaseText { - text: qsTr("Max. fees on %1").arg(!!root.network && root.network.chainName) - - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - - font.pixelSize: 13 - color: Theme.palette.baseColor1 - } - - Item {Layout.fillWidth: true } - - ColumnLayout { - StatusBaseText { - text: root.maxFeesText - - Layout.alignment: Qt.AlignRight - - font.pixelSize: 13 - color: root.enoughFunds ? Theme.palette.directColor1 : Theme.palette.dangerColor1 - } - - StatusBaseText { - text: root.maxFeesEthText - - Layout.alignment: Qt.AlignRight - - font.pixelSize: 13 - color: root.enoughFunds ? Theme.palette.baseColor1 : Theme.palette.dangerColor1 - } - } - } - } - } - } - } - - header: StatusDialogHeader { - leftComponent: Item { - width: 46 - height: 46 - - StatusSmartIdenticon { - anchors.fill: parent - anchors.margins: 3 - - asset: StatusAssetSettings { - width: 40 - height: 40 - bgRadius: bgWidth / 2 - imgIsIdenticon: false - isImage: true - useAcronymForLetterIdenticon: false - name: root.dappIcon - } - bridgeBadge.visible: true - bridgeBadge.width: 16 - bridgeBadge.height: 16 - bridgeBadge.image.source: "assets/sign.svg" - bridgeBadge.border.width: 3 - bridgeBadge.border.color: "transparent" - bridgeBadge.color: Theme.palette.miscColor1 - } - } - headline.title: qsTr("Sign request") - headline.subtitle: root.dappUrl - } - - footer: StatusDialogFooter { - id: footer - - leftButtons: ObjectModel { - MaxFeesDisplay { - maxFeesText: root.maxFeesText - feesTextColor: root.enoughFunds ? Theme.palette.directColor1 : Theme.palette.dangerColor1 - } - Item { - width: 20 - } - EstimatedTimeDisplay { - estimatedTimeText: root.estimatedTimeText - visible: !!root.estimatedTimeText - } - } - - rightButtons: ObjectModel { - StatusButton { - objectName: "rejectButton" - - height: 44 - text: qsTr("Reject") - - onClicked: { - root.reject() - } - } - StatusButton { - height: 44 - text: qsTr("Sign") - - onClicked: { - root.sign() - } - } - } - } - - QtObject { - id: d - - property string payloadToDisplay: "" - property string userDisplayNaming: "" - - function isTransaction() { - return root.method === SessionRequest.methods.signTransaction.name || root.method === SessionRequest.methods.sendTransaction.name - } - - function updateDisplay() { - if (!root.payloadData) - return - - switch (root.method) { - case SessionRequest.methods.personalSign.name: { - payloadToDisplay = SessionRequest.methods.personalSign.getMessageFromData(root.payloadData) - userDisplayNaming = SessionRequest.methods.personalSign.requestDisplay - break - } - case SessionRequest.methods.sign.name: { - payloadToDisplay = SessionRequest.methods.sign.getMessageFromData(root.payloadData) - userDisplayNaming = SessionRequest.methods.sign.requestDisplay - break - } - case SessionRequest.methods.signTypedData_v4.name: { - let messageObject = SessionRequest.methods.signTypedData_v4.getMessageFromData(root.payloadData) - payloadToDisplay = JSON.stringify(JSON.parse(messageObject), null, 2) - userDisplayNaming = SessionRequest.methods.signTypedData_v4.requestDisplay - break - } - case SessionRequest.methods.signTypedData.name: { - let messageObject = SessionRequest.methods.signTypedData.getMessageFromData(root.payloadData) - payloadToDisplay = JSON.stringify(JSON.parse(messageObject), null, 2) - userDisplayNaming = SessionRequest.methods.signTypedData.requestDisplay - break - } - case SessionRequest.methods.signTransaction.name: { - let tx = SessionRequest.methods.signTransaction.getTxObjFromData(root.payloadData) - payloadToDisplay = JSON.stringify(tx, null, 2) - userDisplayNaming = SessionRequest.methods.signTransaction.requestDisplay - break - } - case SessionRequest.methods.sendTransaction.name: { - let tx = SessionRequest.methods.sendTransaction.getTxObjFromData(root.payloadData) - payloadToDisplay = JSON.stringify(tx, null, 2) - userDisplayNaming = SessionRequest.methods.sendTransaction.requestDisplay - break - } - } - } - } -} diff --git a/ui/imports/shared/popups/walletconnect/DAppSignRequestModal.qml b/ui/imports/shared/popups/walletconnect/DAppSignRequestModal.qml new file mode 100644 index 0000000000..613138eba9 --- /dev/null +++ b/ui/imports/shared/popups/walletconnect/DAppSignRequestModal.qml @@ -0,0 +1,192 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQml.Models 2.15 + +import StatusQ 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils +import AppLayouts.Wallet.popups 1.0 +import AppLayouts.Wallet.panels 1.0 + +import shared.popups.walletconnect.panels 1.0 + +import utils 1.0 + +SignTransactionModalBase { + id: root + + required property bool signingTransaction + // DApp info + required property url dappUrl + required property url dappIcon + required property string dappName + // Payload to sign + required property string requestPayload + // Account + required property color accountColor + required property string accountName + required property string accountEmoji + required property string accountAddress + // Network + required property string networkName + required property string networkIconPath + // Fees + required property string currentCurrency + required property string fiatFees + required property string cryptoFees + required property string estimatedTime + required property bool hasFees + + property bool enoughFundsForTransaction: true + property bool enoughFundsForFees: false + + signButtonEnabled: enoughFundsForTransaction && enoughFundsForFees + title: qsTr("Sign Request") + subtitle: SQUtils.StringUtils.extractDomainFromLink(root.dappUrl) + headerIconComponent: RoundImageWithBadge { + imageUrl: root.dappIcon + width: 40 + height: 40 + } + + 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) + + fromImageSmartIdenticon.asset.name: "filled-account" + fromImageSmartIdenticon.asset.emoji: root.accountEmoji + fromImageSmartIdenticon.asset.color: root.accountColor + fromImageSmartIdenticon.asset.isLetterIdenticon: !!root.accountEmoji + toImageSmartIdenticon.asset.name: Style.svg("sign") + toImageSmartIdenticon.asset.bgColor: Theme.palette.primaryColor3 + toImageSmartIdenticon.asset.width: 24 + toImageSmartIdenticon.asset.height: 24 + toImageSmartIdenticon.asset.color: Theme.palette.primaryColor1 + + infoTagText: qsTr("Only sign if you trust the dApp") + infoTag.states: [ + State { + name: "insufficientFunds" + when: !root.enoughFundsForTransaction + PropertyChanges { + target: infoTag + asset.color: Theme.palette.dangerColor1 + tagPrimaryLabel.color: Theme.palette.dangerColor1 + backgroundColor: Theme.palette.dangerColor3 + bgBorderColor: Theme.palette.dangerColor2 + tagPrimaryLabel.text: qsTr("Insufficient funds for transaction") + } + } + ] + showHeaderDivider: !root.requestPayload + + leftFooterContents: ObjectModel { + RowLayout { + Layout.leftMargin: 4 + spacing: Style.current.bigPadding + ColumnLayout { + spacing: 2 + StatusBaseText { + text: qsTr("Max fees:") + color: Theme.palette.baseColor1 + font.pixelSize: Style.current.additionalTextSize + } + StatusTextWithLoadingState { + Layout.fillWidth: true + objectName: "footerFiatFeesText" + text: "%1 %2".arg(formatBigNumber(root.fiatFees)).arg(root.currentCurrency) + loading: root.feesLoading + customColor: root.enoughFundsForFees ? Theme.palette.directColor1 : Theme.palette.dangerColor1 + elide: Qt.ElideMiddle + Binding on text { + when: !root.hasFees + value: qsTr("No fees") + } + } + } + ColumnLayout { + spacing: 2 + visible: root.hasFees + StatusBaseText { + text: qsTr("Est. time:") + color: Theme.palette.baseColor1 + font.pixelSize: Style.current.additionalTextSize + } + StatusTextWithLoadingState { + objectName: "footerEstimatedTime" + text: root.estimatedTime + loading: root.feesLoading + } + } + } + } + + // Payload + ContentPanel { + Layout.fillWidth: true + Layout.fillHeight: true + payloadToDisplay: root.requestPayload + visible: !!root.requestPayload + } + + // Account + SignInfoBox { + Layout.fillWidth: true + Layout.bottomMargin: Style.current.bigPadding + objectName: "accountBox" + caption: qsTr("Sign with") + primaryText: root.accountName + secondaryText: SQUtils.Utils.elideAndFormatWalletAddress(root.accountAddress) + asset.name: "filled-account" + asset.emoji: root.accountEmoji + asset.color: root.accountColor + asset.isLetterIdenticon: !!root.accountEmoji + } + + // Network + SignInfoBox { + Layout.fillWidth: true + Layout.bottomMargin: Style.current.bigPadding + objectName: "networkBox" + caption: qsTr("Network") + primaryText: root.networkName + icon: root.networkIconPath + } + + // Fees + SignInfoBox { + Layout.fillWidth: true + Layout.bottomMargin: Style.current.bigPadding + objectName: "feesBox" + caption: qsTr("Fees") + primaryText: qsTr("Max. fees on %1").arg(root.networkName) + secondaryText: " " + enabled: false + visible: root.hasFees + components: [ + ColumnLayout { + spacing: 2 + StatusTextWithLoadingState { + objectName: "fiatFeesText" + Layout.alignment: Qt.AlignRight + text: "%1 %2".arg(formatBigNumber(root.fiatFees)).arg(root.currentCurrency) + horizontalAlignment: Text.AlignRight + font.pixelSize: Style.current.additionalTextSize + loading: root.feesLoading + customColor: root.enoughFundsForFees ? Theme.palette.directColor1 : Theme.palette.dangerColor1 + } + StatusTextWithLoadingState { + objectName: "cryptoFeesText" + Layout.alignment: Qt.AlignRight + text: "%1 ETH".arg(formatBigNumber(root.cryptoFees)) + horizontalAlignment: Text.AlignRight + font.pixelSize: Style.current.additionalTextSize + customColor: root.enoughFundsForFees ? Theme.palette.baseColor1 : Theme.palette.dangerColor1 + loading: root.feesLoading + } + } + ] + } +} \ No newline at end of file diff --git a/ui/imports/shared/popups/walletconnect/RoundImageWithBadge.qml b/ui/imports/shared/popups/walletconnect/RoundImageWithBadge.qml index e476a3de66..d22969b858 100644 --- a/ui/imports/shared/popups/walletconnect/RoundImageWithBadge.qml +++ b/ui/imports/shared/popups/walletconnect/RoundImageWithBadge.qml @@ -39,15 +39,15 @@ Item { anchors.fill: mainImage active: !mainImage.visible sourceComponent: StatusRoundedComponent { + id: imageWrapper color: Theme.palette.primaryColor3 StatusIcon { - anchors.fill: parent - anchors.margins: Style.current.padding + anchors.fill: imageWrapper + anchors.margins: imageWrapper.width / 4.5 color: Theme.palette.primaryColor1 icon: "dapp" } - } - } + } } layer.enabled: true layer.effect: OpacityMask { @@ -72,7 +72,7 @@ Item { StatusRoundIcon { id: badge - width: (root.width / 2) - Style.current.padding + width: root.width / 3.6 height: width anchors.bottom: parent.bottom anchors.right: parent.right diff --git a/ui/imports/shared/popups/walletconnect/panels/ContentPanel.qml b/ui/imports/shared/popups/walletconnect/panels/ContentPanel.qml index df35e13b89..b2dd16a73b 100644 --- a/ui/imports/shared/popups/walletconnect/panels/ContentPanel.qml +++ b/ui/imports/shared/popups/walletconnect/panels/ContentPanel.qml @@ -1,9 +1,13 @@ import QtQuick 2.15 import QtQuick.Layouts 1.15 +import QtGraphicalEffects 1.15 +import StatusQ.Controls 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 +import utils 1.0 + Rectangle { id: root @@ -14,40 +18,60 @@ Rectangle { color: "transparent" radius: 8 - implicitHeight: contentScrollView.implicitHeight + (2 * contentText.anchors.margins) + implicitHeight: d.expanded ? contentText.implicitHeight + (2 * contentText.anchors.margins) : + Math.min(contentText.implicitHeight + (2 * contentText.anchors.margins), 200) - MouseArea { - anchors.fill: parent - cursorShape: contentScrollView.enabled || !enabled ? undefined : Qt.PointingHandCursor - enabled: contentScrollView.height < contentScrollView.contentHeight - - onClicked: { - contentScrollView.enabled = !contentScrollView.enabled - } - z: contentScrollView.z + 1 + HoverHandler { + id: hoverHandler + target: root } - StatusScrollView { - id: contentScrollView + StatusBaseText { + id: contentText + objectName: "textContent" + anchors.fill: parent + anchors.margins: 20 - contentWidth: availableWidth - contentHeight: contentText.contentHeight + text: root.payloadToDisplay + font.pixelSize: Style.current.additionalTextSize + lineHeightMode: Text.FixedHeight + lineHeight: 18 - padding: 0 + wrapMode: Text.WrapAnywhere - enabled: false - - StatusBaseText { - id: contentText - anchors.fill: parent - anchors.margins: 20 - - width: contentScrollView.availableWidth - - text: root.payloadToDisplay - - wrapMode: Text.WrapAnywhere + StatusFlatButton { + objectName: "expandButton" + anchors.top: parent.top + anchors.right: parent.right + icon.name: d.expanded ? "collapse" : "expand" + icon.color: hovered ? Theme.palette.directColor1 : Theme.palette.baseColor1 + hoverColor: "transparent" + visible: d.canExpand && hoverHandler.hovered + onClicked: { + d.expanded = !d.expanded + } } + + layer.enabled: !d.expanded && d.canExpand + layer.effect: OpacityMask { + maskSource: Rectangle { + width: root.width + height: root.height + gradient: Gradient { + orientation: Gradient.Vertical + GradientStop { position: 0.0 } + GradientStop { position: (root.height - 60) / root.height } + GradientStop { position: 1; color: "transparent" } + } + } + } + } + + QtObject { + id: d + readonly property int maxContentHeight: 350 + property bool expanded: false + property bool canExpand: contentText.implicitHeight > maxContentHeight } } diff --git a/ui/imports/shared/popups/walletconnect/qmldir b/ui/imports/shared/popups/walletconnect/qmldir index db9ca02679..8086638a23 100644 --- a/ui/imports/shared/popups/walletconnect/qmldir +++ b/ui/imports/shared/popups/walletconnect/qmldir @@ -2,6 +2,6 @@ PairWCModal 1.0 PairWCModal.qml DAppsListPopup 1.0 DAppsListPopup.qml ConnectDAppModal 1.0 ConnectDAppModal.qml ConnectionStatusTag 1.0 ConnectionStatusTag.qml -DAppRequestModal 1.0 DAppRequestModal.qml +DAppSignRequestModal 1.0 DAppSignRequestModal.qml DAppsUriCopyInstructionsPopup 1.0 DAppsUriCopyInstructionsPopup.qml RoundImageWithBadge 1.0 RoundImageWithBadge.qml diff --git a/ui/imports/shared/stores/DAppsStore.qml b/ui/imports/shared/stores/DAppsStore.qml index f2c408a2b8..236b5c8f22 100644 --- a/ui/imports/shared/stores/DAppsStore.qml +++ b/ui/imports/shared/stores/DAppsStore.qml @@ -6,7 +6,6 @@ QObject { id: root required property var controller - /// \c dappsJson serialized from status-go.wallet.GetDapps signal dappsListReceived(string dappsJson) signal userAuthenticated(string topic, string id, string password, string pin)