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)