diff --git a/storybook/pages/SendSignModalPage.qml b/storybook/pages/SendSignModalPage.qml index ffc7470197b..8ec54f985ca 100644 --- a/storybook/pages/SendSignModalPage.qml +++ b/storybook/pages/SendSignModalPage.qml @@ -78,9 +78,9 @@ SplitView { formatBigNumber: (number, symbol, noSymbolOption) => parseFloat(number).toLocaleString(Qt.locale(), 'f', 2) + (noSymbolOption ? "" : " " + (symbol || Qt.locale().currencySymbol(Locale.CurrencyIsoCode))) - fromTokenSymbol: ctrlFromSymbol.text - fromTokenAmount: ctrlFromAmount.text - fromTokenContractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F" + tokenSymbol: ctrlFromSymbol.text + tokenAmount: ctrlFromAmount.text + tokenContractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F" accountName: priv.selectedAccount.name accountAddress: priv.selectedAccount.address @@ -98,6 +98,7 @@ SplitView { cryptoFees: formatBigNumber(0.06, "ETH") estimatedTime: qsTr("> 5 minutes") + isCollectibleLoading: isCollectibleLoadingCheckbox.checked isCollectible: isCollectibleCheckbox.checked collectibleContractAddress: !!collectibleComboBox.currentCollectible ? collectibleComboBox.currentCollectible.contractAddress: "" @@ -152,6 +153,10 @@ SplitView { id: isCollectibleCheckbox text:"is collectible" } + CheckBox { + id: isCollectibleLoadingCheckbox + text:"is collectible loading" + } ComboBox { id: collectibleComboBox property var currentCollectible diff --git a/storybook/qmlTests/tests/tst_SendSignModal.qml b/storybook/qmlTests/tests/tst_SendSignModal.qml index 8e04c3c708e..5d4476d2fe4 100644 --- a/storybook/qmlTests/tests/tst_SendSignModal.qml +++ b/storybook/qmlTests/tests/tst_SendSignModal.qml @@ -24,9 +24,9 @@ Item { formatBigNumber: (number, symbol, noSymbolOption) => parseFloat(number).toLocaleString(Qt.locale(), 'f', 2) + (noSymbolOption ? "" : " " + symbol) - fromTokenSymbol: "DAI" - fromTokenAmount: "100.07" - fromTokenContractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F" + tokenSymbol: "DAI" + tokenAmount: "100.07" + tokenContractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F" accountName: "Hot wallet (generated)" accountAddress: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881" @@ -101,15 +101,15 @@ Item { function test_fromToProps() { verify(!!controlUnderTest) - controlUnderTest.fromTokenSymbol = "DAI" - controlUnderTest.fromTokenAmount = "1000.123456789" - controlUnderTest.fromTokenContractAddress = "Oxdeadbeef" + controlUnderTest.tokenSymbol = "DAI" + controlUnderTest.tokenAmount = "1000.123456789" + controlUnderTest.tokenContractAddress = "Oxdeadbeef" // title compare(controlUnderTest.title, qsTr("Sign Send")) // subtitle - compare(controlUnderTest.subtitle, qsTr("%1 to %2").arg(controlUnderTest.formatBigNumber(controlUnderTest.fromTokenAmount, controlUnderTest.fromTokenSymbol)) + compare(controlUnderTest.subtitle, qsTr("%1 %2 to %3").arg(controlUnderTest.tokenAmount).arg(controlUnderTest.tokenSymbol) .arg(SQUtils.Utils.elideAndFormatWalletAddress(controlUnderTest.recipientAddress))) compare(controlUnderTest.gradientColor, controlUnderTest.accountColor) @@ -120,8 +120,8 @@ Item { // info box const headerText = findChild(controlUnderTest.contentItem, "headerText") verify(!!headerText) - compare(headerText.text, qsTr("Send %1 to %2 on %3"). - arg(controlUnderTest.formatBigNumber(controlUnderTest.fromTokenAmount, controlUnderTest.fromTokenSymbol)). + compare(headerText.text, qsTr("Send %1 %2 to %3 on %4"). + arg(controlUnderTest.tokenAmount).arg(controlUnderTest.tokenSymbol). arg(SQUtils.Utils.elideAndFormatWalletAddress(controlUnderTest.recipientAddress)).arg(controlUnderTest.networkName)) const fromImage = findChild(controlUnderTest.contentItem, "fromImageIdenticon") verify(!!fromImage) @@ -132,14 +132,14 @@ Item { const toImage = findChild(controlUnderTest.contentItem, "toImageIdenticon") verify(!!toImage) - compare(toImage.asset.name, Constants.tokenIcon(controlUnderTest.fromTokenSymbol)) + compare(toImage.asset.name, Constants.tokenIcon(controlUnderTest.tokenSymbol)) // send box const sendBox = findChild(controlUnderTest.contentItem, "sendAssetBox") verify(!!sendBox) compare(sendBox.caption, qsTr("Send")) - compare(sendBox.primaryText, "1,000.12 DAI") - compare(sendBox.secondaryText, SQUtils.Utils.elideAndFormatWalletAddress(controlUnderTest.fromTokenContractAddress)) + compare(sendBox.primaryText, "1000.123456789 DAI") + compare(sendBox.secondaryText, SQUtils.Utils.elideAndFormatWalletAddress(controlUnderTest.tokenContractAddress)) } function test_tokenContextmenu() { @@ -165,14 +165,14 @@ Item { const externalLink = findChild(contextMenu, "externalLink") verify(!!externalLink) - compare(externalLink.text, !!controlUnderTest.fromTokenSymbol ? - qsTr("View %1 %2 contract address on %3").arg(controlUnderTest.networkName).arg(controlUnderTest.fromTokenSymbol).arg("Etherscan") + compare(externalLink.text, !!controlUnderTest.tokenSymbol ? + qsTr("View %1 %2 contract address on %3").arg(controlUnderTest.networkName).arg(controlUnderTest.tokenSymbol).arg("Etherscan") : qsTr("View %1 contract address on %2").arg(controlUnderTest.networkName).arg("Etherscan")) compare(externalLink.icon.name, "external-link") externalLink.triggered() tryCompare(signalSpyOpenLink, "count", 1) compare(signalSpyOpenLink.signalArguments[0][0], - "%1/%2/%3".arg(controlUnderTest.networkBlockExplorerUrl).arg(Constants.networkExplorerLinks.addressPath).arg(controlUnderTest.fromTokenContractAddress)) + "%1/%2/%3".arg(controlUnderTest.networkBlockExplorerUrl).arg(Constants.networkExplorerLinks.addressPath).arg(controlUnderTest.tokenContractAddress)) verify(!contextMenu.opened) contractInfoButtonWithMenu.clicked(0) @@ -274,12 +274,12 @@ Item { verify(!!loadingComponent) verify(loadingComponent.visible) - const fromAccountSmartIdenticon = findChild(controlUnderTest.contentItem, "fromAccountSmartIdenticon") - verify(!!fromAccountSmartIdenticon) - compare(fromAccountSmartIdenticon.asset.name, "filled-account") - compare(fromAccountSmartIdenticon.asset.emoji, controlUnderTest.accountEmoji) - compare(fromAccountSmartIdenticon.asset.color, controlUnderTest.accountColor) - compare(fromAccountSmartIdenticon.asset.isLetterIdenticon, !!controlUnderTest.accountEmoji) + const accountSmartIdenticon = findChild(controlUnderTest.contentItem, "accountSmartIdenticon") + verify(!!accountSmartIdenticon) + compare(accountSmartIdenticon.asset.name, "filled-account") + compare(accountSmartIdenticon.asset.emoji, controlUnderTest.accountEmoji) + compare(accountSmartIdenticon.asset.color, controlUnderTest.accountColor) + compare(accountSmartIdenticon.asset.isLetterIdenticon, !!controlUnderTest.accountEmoji) // send collectible box const collectibleCaption = findChild(controlUnderTest.contentItem, "collectibleCaption") @@ -325,7 +325,7 @@ Item { blockchainExternalLink.triggered() tryCompare(signalSpyOpenLink, "count", 2) compare(signalSpyOpenLink.signalArguments[1][0], - "%1/nft/%2/%3".arg("Etherscan").arg(data.contractAddress).arg(data.tokenId)) + "%1/nft/%2/%3".arg("https://etherscan.io/").arg(data.contractAddress).arg(data.tokenId)) const copyButton = findChild(moreMenu, "copyButton") verify(!!copyButton) diff --git a/ui/app/AppLayouts/Wallet/adaptors/SignSendAdaptor.qml b/ui/app/AppLayouts/Wallet/adaptors/SignSendAdaptor.qml new file mode 100644 index 00000000000..58c609f0b41 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/adaptors/SignSendAdaptor.qml @@ -0,0 +1,99 @@ +import QtQuick 2.15 + +import StatusQ 0.1 +import StatusQ.Core.Utils 0.1 + +/** + Adaptor transforming selected data from send to a format that + can be used in the sign modal +**/ +QObject { + id: root + + /** Account key used for filtering **/ + required property string accountKey + /** network chainId used for filtering **/ + required property int chainId + /** token key used for filtering **/ + required property string tokenKey + /** amount selected in send modal for sending **/ + required property string selectedAmountInBaseUnit + /** + Expected model structure: + + name [int] - name of account + address [string] - address of account + emoji [string] - emoji of account + colorId [string] - colorId of account + **/ + required property var accountsModel + /** + Expected model structure: + + chainId [int] - network chain id + chainName [string] - name of network + iconUrl [string] - network icon url + **/ + required property var networksModel + /** + Expected model structure: + + key [int] - unique id of token + symbol [int] - symbol of token + decimals [string] - decimals of token + **/ + required property var tokenBySymbolModel + + /** output property of the account selected **/ + readonly property var selectedAccount: selectedAccountEntry.item + /** output property of the network selected **/ + readonly property var selectedNetwork: selectedNetworkEntry.item + /** output property of the asset (ERC20) selected **/ + readonly property var selectedAsset: selectedAssetEntry.item + /** output property of the localised amount to send **/ + readonly property string selectedAmount: { + const decimals = !!root.selectedAsset ? root.selectedAsset.decimals: 0 + const divisor = AmountsArithmetic.fromExponent(decimals) + let amount = AmountsArithmetic.div( + AmountsArithmetic.fromString(root.selectedAmountInBaseUnit), + divisor).toFixed(decimals) + // removeDecimalTrailingZeros + amount = Utils.stripTrailingZeros(amount) + // localize + return amount.replace(".", Qt.locale().decimalPoint) + } + /** output property of the selected asset contract address on selected chainId **/ + readonly property string selectedAssetContractAddress: selectedAssetContractEntry.available && + !!selectedAssetContractEntry.item ? + selectedAssetContractEntry.item.address: "" + + ModelEntry { + id: selectedAccountEntry + sourceModel: root.accountsModel + value: root.accountKey + key: "address" + } + + ModelEntry { + id: selectedNetworkEntry + sourceModel: root.networksModel + value: root.chainId + key: "chainId" + } + + ModelEntry { + id: selectedAssetEntry + sourceModel: root.tokenBySymbolModel + value: root.tokenKey + key: "key" + } + + ModelEntry { + id: selectedAssetContractEntry + sourceModel: selectedAssetEntry.available && + !!selectedAssetEntry.item ? + selectedAssetEntry.item.addressPerChain: null + value: root.chainId + key: "chainId" + } +} diff --git a/ui/app/AppLayouts/Wallet/adaptors/qmldir b/ui/app/AppLayouts/Wallet/adaptors/qmldir index cc07adb9f33..944451c7e83 100644 --- a/ui/app/AppLayouts/Wallet/adaptors/qmldir +++ b/ui/app/AppLayouts/Wallet/adaptors/qmldir @@ -1,3 +1,4 @@ CollectiblesSelectionAdaptor 1.0 CollectiblesSelectionAdaptor.qml TokenSelectorViewAdaptor 1.0 TokenSelectorViewAdaptor.qml WalletAccountsSelectorAdaptor 1.0 WalletAccountsSelectorAdaptor.qml +SignSendAdaptor 1.0 SignSendAdaptor.qml diff --git a/ui/app/AppLayouts/Wallet/controls/RouterErrorTag.qml b/ui/app/AppLayouts/Wallet/controls/RouterErrorTag.qml index f11b9a02184..cc144aa8b46 100644 --- a/ui/app/AppLayouts/Wallet/controls/RouterErrorTag.qml +++ b/ui/app/AppLayouts/Wallet/controls/RouterErrorTag.qml @@ -51,6 +51,7 @@ Control { color: Theme.palette.dangerColor1 font.pixelSize: Theme.additionalTextSize + elide: Text.ElideRight } StatusButton { id: addBalanceButton diff --git a/ui/app/AppLayouts/Wallet/panels/RecipientInfoButtonWithMenu.qml b/ui/app/AppLayouts/Wallet/panels/RecipientInfoButtonWithMenu.qml index 3f7e8ef0a58..91db88d47ba 100644 --- a/ui/app/AppLayouts/Wallet/panels/RecipientInfoButtonWithMenu.qml +++ b/ui/app/AppLayouts/Wallet/panels/RecipientInfoButtonWithMenu.qml @@ -1,3 +1,4 @@ +import QtQuick 2.15 import StatusQ 0.1 import StatusQ.Core.Theme 0.1 @@ -9,23 +10,31 @@ import utils 1.0 StatusFlatButton { id: root + /** Input property holding selected recipient address **/ required property string recipientAddress + /** Input property holding selected network name **/ required property string networkName + /** Input property holding selected network short name **/ required property string networkShortName + /** Input property holding selected network explorer url **/ required property string networkBlockExplorerUrl + /** Signal to launch link **/ signal openLink(string link) + QtObject { + id: d + + function getExplorerName() { + return Utils.getChainExplorerName(root.networkShortName) + } + } + icon.name: "more" icon.color: highlighted ? Theme.palette.directColor1 : Theme.palette.directColor5 - highlighted: moreMenu.opened onClicked: moreMenu.popup(root, 0, height + 4) - function getExplorerName() { - return Utils.getChainExplorerName(root.networkShortName) - } - StatusMenu { id: moreMenu objectName: "moreMenu" @@ -33,7 +42,7 @@ StatusFlatButton { StatusAction { objectName: "externalLink" //: e.g. "View receiver address on Etherscan" - text: qsTr("View receiver address on %1").arg(getExplorerName()) + text: qsTr("View receiver address on %1").arg(d.getExplorerName()) icon.name: "external-link" onTriggered: { var link = "%1/%2/%3".arg(root.networkBlockExplorerUrl).arg(Constants.networkExplorerLinks.addressPath).arg(root.recipientAddress) diff --git a/ui/app/AppLayouts/Wallet/panels/SignCollectibleInfoBox.qml b/ui/app/AppLayouts/Wallet/panels/SignCollectibleInfoBox.qml index 813e8538dca..7846f7c9995 100644 --- a/ui/app/AppLayouts/Wallet/panels/SignCollectibleInfoBox.qml +++ b/ui/app/AppLayouts/Wallet/panels/SignCollectibleInfoBox.qml @@ -15,18 +15,30 @@ import AppLayouts.Wallet.views.collectibles 1.0 Control { id: root + /** Input property holding collectible name **/ required property string name - - required property string backgroundColor + /** Input property holding collectible background color **/ + required property color backgroundColor + /** Input property holding if collectible has valid metadata **/ required property bool isMetadataValid + /** Input property holding collectible fallback image url **/ required property string fallbackImageUrl + /** Input property holding collectible contract address **/ required property string contractAddress + /** Input property holding collectible tokenId **/ required property string tokenId + /** Input property holding if collectible is loading **/ + required property bool loading + /** Input property holding network short name **/ required property string networkShortName + /** Input property holding network explorer url **/ required property string networkBlockExplorerUrl + + /** Input property holding openSea explorer url **/ required property string openSeaExplorerUrl + /** Signal to launch link **/ signal openLink(string link) QtObject { @@ -38,10 +50,10 @@ Control { readonly property string collectibleBlockExplorerLink: { if (root.networkShortName === Constants.networkShortChainNames.mainnet) { - return "%1/nft/%2/%3".arg(getExplorerName()).arg(root.contractAddress).arg(root.tokenId) + return "%1/nft/%2/%3".arg(root.networkBlockExplorerUrl).arg(root.contractAddress).arg(root.tokenId) } else { - return "%1/token/%2?a=%3".arg(getExplorerName()).arg(root.contractAddress).arg(root.tokenId) + return "%1/token/%2?a=%3".arg(root.networkBlockExplorerUrl).arg(root.contractAddress).arg(root.tokenId) } } } @@ -73,6 +85,7 @@ Control { Layout.preferredWidth: 40 Layout.preferredHeight: 40 radius: 4 + isCollectibleLoading: root.loading } ColumnLayout { diff --git a/ui/app/AppLayouts/Wallet/popups/SignTransactionModalBase.qml b/ui/app/AppLayouts/Wallet/popups/SignTransactionModalBase.qml index 3c3becbe9b9..a81e79e8667 100644 --- a/ui/app/AppLayouts/Wallet/popups/SignTransactionModalBase.qml +++ b/ui/app/AppLayouts/Wallet/popups/SignTransactionModalBase.qml @@ -48,6 +48,9 @@ StatusDialog { property int expirationSeconds property bool hasExpiryDate: false + // Close hidden explicitely until we have persistent notifications in place to reopen this dialog from outside + property bool headerActionsCloseButtonVisible: false + property ObjectModel leftFooterContents property ObjectModel rightFooterContents: ObjectModel { RowLayout { @@ -91,7 +94,8 @@ StatusDialog { readonly property alias infoTag: infoTag property bool showHeaderDivider: true property bool isCollectible - readonly property alias fromAccountSmartIdenticon: fromAccountSmartIdenticon + property bool isCollectibleLoading + readonly property alias accountSmartIdenticon: accountSmartIdenticon readonly property alias collectibleMedia: collectibleMedia default property alias contents: contentsLayout.data @@ -109,7 +113,8 @@ StatusDialog { visible: root.title || root.subtitle headline.title: root.title headline.subtitle: root.subtitle - actions.closeButton.visible: false // Close hidden explicitely until we have persistent notifications in place to reopen this dialog from outside + actions.closeButton.visible: root.headerActionsCloseButtonVisible + actions.closeButton.onClicked: root.close() leftComponent: root.headerIconComponent } @@ -210,7 +215,7 @@ StatusDialog { RowLayout { Layout.alignment: Qt.AlignCenter - spacing: -fromAccountSmartIdenticon.width+4 + spacing: -accountSmartIdenticon.width+4 Item { height: 120 width: 120 @@ -220,6 +225,7 @@ StatusDialog { objectName: "collectibleMedia" manualMaxDimension: 120 radius: 12 + isCollectibleLoading: root.isCollectibleLoading } layer.enabled: true layer.effect: DropShadow { @@ -233,16 +239,16 @@ StatusDialog { Layout.alignment: Qt.AlignBottom Layout.bottomMargin: -4 - Layout.preferredWidth: fromAccountSmartIdenticon.width + 4 - Layout.preferredHeight: fromAccountSmartIdenticon.height + 4 + Layout.preferredWidth: accountSmartIdenticon.width + 4 + Layout.preferredHeight: accountSmartIdenticon.height + 4 radius: width/2 color: root.backgroundColor StatusSmartIdenticon { - id: fromAccountSmartIdenticon + id: accountSmartIdenticon anchors.centerIn: parent - objectName: "fromAccountSmartIdenticon" + objectName: "accountSmartIdenticon" asset.bgWidth: 28 asset.bgHeight: 28 visible: !!asset.name || !!asset.source diff --git a/ui/app/AppLayouts/Wallet/popups/simpleSend/SendSignModal.qml b/ui/app/AppLayouts/Wallet/popups/simpleSend/SendSignModal.qml index 064ac24d324..89483243043 100644 --- a/ui/app/AppLayouts/Wallet/popups/simpleSend/SendSignModal.qml +++ b/ui/app/AppLayouts/Wallet/popups/simpleSend/SendSignModal.qml @@ -16,75 +16,107 @@ import utils 1.0 SignTransactionModalBase { id: root - required property string fromTokenSymbol - required property string fromTokenAmount - required property string fromTokenContractAddress + /** Input property holding selected token symbol **/ + required property string tokenSymbol + /** Input property holding selected token amount **/ + required property string tokenAmount + /** Input property holding selected token contract address **/ + required property string tokenContractAddress + /** Input property holding selected account name **/ required property string accountName + /** Input property holding selected account address **/ required property string accountAddress + /** Input property holding selected account emoji **/ required property string accountEmoji + /** Input property holding selected account color **/ required property color accountColor /** TODO: Use new recipients appraoch from https://github.com/status-im/status-desktop/issues/16916 **/ required property string recipientAddress - required property string networkShortName // e.g. "oeth" - required property string networkName // e.g. "Optimism" - required property string networkIconPath // e.g. `Theme.svg("network/Network=Optimism")` + /** Input property holding selected network short name **/ + required property string networkShortName + /** Input property holding selected network name **/ + required property string networkName + /** Input property holding selected network icon path + e.g. `Theme.svg("network/Network=Optimism")`**/ + required property string networkIconPath + /** Input property holding selected network blockchain + explorer name **/ required property string networkBlockExplorerUrl + /** Input property holding localised path fees in fiat **/ required property string fiatFees + /** Input property holding localised path fees in crypto **/ required property string cryptoFees + /** Input property holding localised path estimate time **/ required property string estimatedTime - required property string collectibleContractAddress - required property string collectibleTokenId + /** Input property holding selected collectible name **/ required property string collectibleName - required property string collectibleBackgroundColor - required property bool collectibleIsMetadataValid + /** Input property holding selected collectible token id **/ + required property string collectibleTokenId + /** Input property holding selected collectible media url **/ required property string collectibleMediaUrl + /** Input property holding selected collectible media type **/ required property string collectibleMediaType + /** Input property holding selected collectible fallback media url **/ required property string collectibleFallbackImageUrl + /** Input property holding selected collectible contract address **/ + required property string collectibleContractAddress + /** Input property holding selected collectible background color **/ + required property string collectibleBackgroundColor + /** Input property holding selected if collectible meta data is valid **/ + required property bool collectibleIsMetadataValid + /** Input property holding function openSea explorer url for the collectible **/ required property var fnGetOpenSeaExplorerUrl title: qsTr("Sign Send") //: e.g. (Send) 100 DAI to batista.eth subtitle: { - const tokenToSend = root.isCollectible ? - root.collectibleName: - formatBigNumber(fromTokenAmount, fromTokenSymbol) + const tokenToSend = root.isCollectible ? root.collectibleName: + "%1 %2".arg(root.tokenAmount).arg(root.tokenSymbol) return qsTr("%1 to %2"). arg(tokenToSend). arg(SQUtils.Utils.elideAndFormatWalletAddress(root.recipientAddress)) } + headerActionsCloseButtonVisible: true + + // Wallet account background color to be used as gardient in the header gradientColor: root.accountColor + + // In case if selected token is an asset then this displays the account selected fromImageSmartIdenticon.asset.name: "filled-account" fromImageSmartIdenticon.asset.emoji: root.accountEmoji fromImageSmartIdenticon.asset.color: root.accountColor fromImageSmartIdenticon.asset.isLetterIdenticon: !!root.accountEmoji - fromAccountSmartIdenticon.asset.name: "filled-account" - fromAccountSmartIdenticon.asset.emoji: root.accountEmoji - fromAccountSmartIdenticon.asset.color: root.accountColor - fromAccountSmartIdenticon.asset.isLetterIdenticon: !!root.accountEmoji - fromAccountSmartIdenticon.asset.isImage: root.isCollectible - - toImageSource: Constants.tokenIcon(root.fromTokenSymbol) + // In case if selected token is an asset then this displays the token selected + toImageSource: Constants.tokenIcon(root.tokenSymbol) + // Collectible data in header in case a collectible is selected collectibleMedia.backgroundColor: root.collectibleBackgroundColor collectibleMedia.isMetadataValid: root.collectibleIsMetadataValid collectibleMedia.mediaUrl: root.collectibleMediaUrl collectibleMedia.mediaType: root.collectibleMediaType collectibleMedia.fallbackImageUrl: root.collectibleFallbackImageUrl + /** In case if selected token is an collectible then + this displays the account selected as badge **/ + accountSmartIdenticon.asset.name: "filled-account" + accountSmartIdenticon.asset.emoji: root.accountEmoji + accountSmartIdenticon.asset.color: root.accountColor + accountSmartIdenticon.asset.isLetterIdenticon: !!root.accountEmoji + accountSmartIdenticon.asset.isImage: root.isCollectible + //: e.g. "Send 100 DAI to recipient on " headerMainText: { - const tokenToSend = root.isCollectible ? - root.collectibleName: - formatBigNumber(fromTokenAmount, fromTokenSymbol) + const tokenToSend = root.isCollectible ? root.collectibleName: + "%1 %2".arg(root.tokenAmount).arg(root.tokenSymbol) return qsTr("Send %1 to %2 on %3").arg(tokenToSend) .arg(SQUtils.Utils.elideAndFormatWalletAddress(root.recipientAddress)).arg(root.networkName) } @@ -143,10 +175,10 @@ SignTransactionModalBase { Layout.fillWidth: true Layout.bottomMargin: Theme.bigPadding caption: qsTr("Send") - primaryText: formatBigNumber(root.fromTokenAmount, root.fromTokenSymbol) - secondaryText: root.fromTokenSymbol !== Constants.ethToken ? - SQUtils.Utils.elideAndFormatWalletAddress(root.fromTokenContractAddress) : "" - icon: Constants.tokenIcon(root.fromTokenSymbol) + primaryText: "%1 %2".arg(root.tokenAmount).arg(root.tokenSymbol) + secondaryText: root.tokenSymbol !== Constants.ethToken ? + SQUtils.Utils.elideAndFormatWalletAddress(root.tokenContractAddress) : "" + icon: Constants.tokenIcon(root.tokenSymbol) badge: root.networkIconPath highlighted: contractInfoButtonWithMenu.hovered components: [ @@ -154,9 +186,9 @@ SignTransactionModalBase { id: contractInfoButtonWithMenu objectName: "contractInfoButtonWithMenu" - visible: root.fromTokenSymbol !== Constants.ethToken - symbol: root.fromTokenSymbol - contractAddress: root.fromTokenContractAddress + visible: root.tokenSymbol !== Constants.ethToken + symbol: root.tokenSymbol + contractAddress: root.tokenContractAddress networkName: root.networkName networkShortName: root.networkShortName networkBlockExplorerUrl: root.networkBlockExplorerUrl @@ -185,6 +217,7 @@ SignTransactionModalBase { tokenId: root.collectibleTokenId networkShortName: root.networkShortName networkBlockExplorerUrl: root.networkBlockExplorerUrl + loading: root.isCollectibleLoading openSeaExplorerUrl: root.fnGetOpenSeaExplorerUrl(root.networkShortName) onOpenLink: (link) => root.openLinkWithConfirmation(link) } diff --git a/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml b/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml index b3bc4b291ac..560182c05bd 100644 --- a/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml +++ b/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml @@ -119,6 +119,9 @@ StatusDialog { !amountToSend.markAsInvalid && amountToSend.valid + /** Output property if a collectible is selected in the the send modal **/ + readonly property bool isCollectibleSelected: d.isCollectibleSelected + /** TODO: replace with new and improved recipient selector StatusDateRangePicker TBD under https://github.com/status-im/status-desktop/issues/16916 **/ required property var savedAddressesModel @@ -487,7 +490,9 @@ StatusDialog { estimatedFees: root.estimatedFiatFees error: d.errNotEnoughGas - errorTags: amountToSend.markAsInvalid || !!root.routerErrorCode ? + errorTags: amountToSend.markAsInvalid || + !!root.routerErrorCode || + !!root.routerError? errorTagsModel: null loading: root.routesLoading && root.allValuesFilledCorrectly @@ -509,10 +514,12 @@ StatusDialog { errorDetails: !d.errNotEnoughGas ? root.routerErrorDetails: "" buttonText: qsTr("Add ETH") - expandable: !d.errNotEnoughGas + expandable: !d.errNotEnoughGas || + !(!root.routerErrorCode && + !!root.routerError) onButtonClicked: root.launchBuyFlow() - visible: !!root.routerErrorCode + visible: !!root.routerErrorCode || !!root.routerError } } } diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml index 60a512d42c1..a5c20a4edb2 100644 --- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml @@ -465,6 +465,12 @@ QtObject { return root.areTestNetworksEnabled ? Constants.openseaExplorerLinks.testnetLink : Constants.openseaExplorerLinks.mainnetLink } + function getOpenSeaUrl(networkShortName) { + let networkName = getOpenSeaNetworkName(networkShortName) + let baseLink = root.areTestNetworksEnabled ? Constants.openseaExplorerLinks.testnetLink : Constants.openseaExplorerLinks.mainnetLink + return "%1/assets/%2".arg(baseLink).arg(networkName) + } + function getOpenSeaCollectionUrl(networkShortName, contractAddress) { let networkName = getOpenSeaNetworkName(networkShortName) let baseLink = root.areTestNetworksEnabled ? Constants.openseaExplorerLinks.testnetLink : Constants.openseaExplorerLinks.mainnetLink diff --git a/ui/app/AppLayouts/Wallet/views/SendModalFooter.qml b/ui/app/AppLayouts/Wallet/views/SendModalFooter.qml index e72e2021b71..ccb2cc1be2b 100644 --- a/ui/app/AppLayouts/Wallet/views/SendModalFooter.qml +++ b/ui/app/AppLayouts/Wallet/views/SendModalFooter.qml @@ -92,7 +92,7 @@ StatusDialogFooter { disabledColor: Theme.palette.directColor8 enabled: !!root.estimatedTime && !!root.estimatedFees && - !root.loading + !root.loading && !root.error text: qsTr("Review Send") diff --git a/ui/app/AppLayouts/Wallet/views/collectibles/CollectibleMedia.qml b/ui/app/AppLayouts/Wallet/views/collectibles/CollectibleMedia.qml index a8d447dc8b4..ebaac3bc5da 100644 --- a/ui/app/AppLayouts/Wallet/views/collectibles/CollectibleMedia.qml +++ b/ui/app/AppLayouts/Wallet/views/collectibles/CollectibleMedia.qml @@ -16,7 +16,7 @@ StatusRoundedMedia { QtObject { id: d - property bool isUnknown: root.isError && root.componentMediaType === StatusRoundedMedia.MediaType.Unknown + readonly property bool isUnknown: root.isError && root.componentMediaType === StatusRoundedMedia.MediaType.Unknown } radius: Theme.radius diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 7a0910ac3c6..03cab7826db 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -679,15 +679,29 @@ Item { savedAddressesModel: WalletStores.RootStore.savedAddresses recentRecipientsModel: appMain.transactionStore.tempActivityController1Model + isDetailedCollectibleLoading: appMain.walletCollectiblesStore.isDetailedCollectibleLoading + detailedCollectible: appMain.walletCollectiblesStore.detailedCollectible + currentCurrency: appMain.currencyStore.currentCurrency fnFormatCurrencyAmount: appMain.currencyStore.formatCurrencyAmount fnFormatCurrencyAmountFromBigInt: appMain.currencyStore.formatCurrencyAmountFromBigInt - // TODO remove this call to mainModule under #16919 fnResolveENS: function(ensName, uuid) { appMain.rootStore.resolveENS(ensName, uuid) } + fnGetDetailedCollectible: function(chainId, contractAddress, tokenId) { + appMain.walletCollectiblesStore.getDetailedCollectible(chainId, contractAddress, tokenId) + } + + fnResetDetailedCollectible: function() { + appMain.walletCollectiblesStore.resetDetailedCollectible() + } + + fnGetOpenSeaUrl: function(networkShortName) { + return WalletStores.RootStore.getOpenSeaUrl(networkShortName) + } + onLaunchBuyFlowRequested: { d.buyFormData.selectedWalletAddress = accountAddress d.buyFormData.selectedNetworkChainId = chainId diff --git a/ui/app/mainui/SendModalHandler.qml b/ui/app/mainui/SendModalHandler.qml index 5eb67c7bd70..bc262bd45cc 100644 --- a/ui/app/mainui/SendModalHandler.qml +++ b/ui/app/mainui/SendModalHandler.qml @@ -1,9 +1,11 @@ import QtQuick 2.15 +import QtQuick.Controls 2.15 import SortFilterProxyModel 0.2 import StatusQ 0.1 import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 import StatusQ.Core.Utils 0.1 as SQUtils import AppLayouts.Wallet.stores 1.0 as WalletStores @@ -105,18 +107,47 @@ QtObject { required property var savedAddressesModel required property var recentRecipientsModel - /** required function to resolve an ens name **/ + /** required function to resolve an ens name + - ensName [string] - ensName to be resolved + - uuid [string] - unique identifier for the request + **/ required property var fnResolveENS /** required signal to receive resolved ens name address **/ signal ensNameResolved(string resolvedPubKey, string resolvedAddress, string uuid) /** curently selected fiat currency symbol **/ required property string currentCurrency - /** required function to format currency amount to locale string **/ + /** required function to format currency amount to locale string + - amount [real] - amount to be formatted as a number + - symbol [string] - fiat/crypto currency symbol + **/ required property var fnFormatCurrencyAmount - /** required function to format to currency amount from big int **/ + /** required function to format to currency amount from big int + - amount [real] - amount to be formatted as a number + - symbol [string] - fiat/crypto currency symbol + - decimals [int] - decimals of the crypto token + **/ required property var fnFormatCurrencyAmountFromBigInt + /** required property holds the detailed collectible **/ + required property var detailedCollectible + /** required property holds if collectible details is loading **/ + required property bool isDetailedCollectibleLoading + /** required function to fetch detailed collectible + chainId, contractAddress, tokenId + - chainId [int] - chainId of collectible + - contractAddress [string] - contract address of collectible + - tokenId [string] - token id of collectible + **/ + required property var fnGetDetailedCollectible + /** required function to reset the detailed collectible **/ + required property var fnResetDetailedCollectible + + /** required function to get openSea explorer url + - networkShortName [string] - collectible networks short name + **/ + required property var fnGetOpenSeaUrl + /** signal to request launch of buy crypto modal **/ signal launchBuyFlowRequested(string accountAddress, int chainId, string tokenKey) @@ -256,10 +287,10 @@ QtObject { SimpleSendModal { id: simpleSendModal - accountsModel: backendHandler.accountsSelectorAdaptor.processedWalletAccounts - assetsModel: backendHandler.assetsSelectorViewAdaptor.outputAssetsModel - collectiblesModel: backendHandler.collectiblesSelectionAdaptor.model - networksModel: backendHandler.filteredFlatNetworksModel + accountsModel: handler.accountsSelectorAdaptor.processedWalletAccounts + assetsModel: handler.assetsSelectorViewAdaptor.outputAssetsModel + collectiblesModel: handler.collectiblesSelectionAdaptor.model + networksModel: handler.filteredFlatNetworksModel savedAddressesModel: root.savedAddressesModel recentRecipientsModel: root.recentRecipientsModel @@ -273,11 +304,11 @@ QtObject { } onFormChanged: { - backendHandler.resetRouterValues() + handler.resetRouterValues() if(allValuesFilledCorrectly) { - backendHandler.uuid = Utils.uuid() + handler.uuid = Utils.uuid() simpleSendModal.routesLoading = true - root.transactionStoreNew.fetchSuggestedRoutes(backendHandler.uuid, + root.transactionStoreNew.fetchSuggestedRoutes(handler.uuid, sendType, selectedChainId, selectedAccountAddress, @@ -287,16 +318,22 @@ QtObject { } } - // TODO: this should be called from the Reiew and Sign Modal instead onReviewSendClicked: { - root.transactionStoreNew.authenticateAndTransfer(backendHandler.uuid, selectedAccountAddress) + if (handler.selectedCollectibleEntry.available && + !!handler.selectedCollectibleEntry.item) { + root.fnGetDetailedCollectible(simpleSendModal.selectedChainId , + handler.selectedCollectibleEntry.item.contractAddress, + handler.selectedCollectibleEntry.item.tokenId) + } + Global.openPopup(sendSignModalCmp) } onLaunchBuyFlow: { root.launchBuyFlowRequested(selectedAccountAddress, selectedChainId, selectedTokenKey) } - readonly property var backendHandler: QtObject { + QtObject { + id: handler property string uuid property var fetchedPathModel @@ -307,7 +344,7 @@ QtObject { function routesFetched(returnedUuid, pathModel, errCode, errDescription) { simpleSendModal.routesLoading = false - if(returnedUuid !== uuid) { + if(returnedUuid !== handler.uuid) { // Suggested routes for a different fetch, ignore return } @@ -319,7 +356,7 @@ QtObject { } function transactionSent(returnedUuid, chainId, approvalTx, txHash, error) { - if(returnedUuid !== uuid) { + if(returnedUuid !== handler.uuid) { // Suggested routes for a different fetch, ignore return } @@ -327,7 +364,7 @@ QtObject { if (error.includes(Constants.walletSection.authenticationCanceled)) { return } - // TODO: handle error here + simpleSendModal.routerError = error return } close() @@ -337,7 +374,7 @@ QtObject { accounts: root.walletAccountsModel assetsModel: root.groupedAccountAssetsModel tokensBySymbolModel: root.plainTokensBySymbolModel - filteredFlatNetworksModel: backendHandler.filteredFlatNetworksModel + filteredFlatNetworksModel: handler.filteredFlatNetworksModel selectedTokenKey: simpleSendModal.selectedTokenKey selectedNetworkChainId: simpleSendModal.selectedChainId @@ -346,7 +383,6 @@ QtObject { } readonly property var assetsSelectorViewAdaptor: TokenSelectorViewAdaptor { - // TODO: remove all store dependecies and add specific properties to the handler instead assetsModel: root.groupedAccountAssetsModel flatNetworksModel: root.flatNetworksModel @@ -361,7 +397,7 @@ QtObject { accountKey: simpleSendModal.selectedAccountAddress enabledChainIds: [simpleSendModal.selectedChainId] - networksModel: backendHandler.filteredFlatNetworksModel + networksModel: handler.filteredFlatNetworksModel collectiblesModel: SortFilterProxyModel { sourceModel: root.collectiblesBySymbolModel filters: ValueFilter { @@ -372,8 +408,8 @@ QtObject { } readonly property var totalFeesAggregator: FunctionAggregator { - model: !!backendHandler.fetchedPathModel ? - backendHandler.fetchedPathModel: null + model: !!handler.fetchedPathModel ? + handler.fetchedPathModel: null initialValue: "0" roleName: "txTotalFee" @@ -382,8 +418,8 @@ QtObject { SQUtils.AmountsArithmetic.fromString(value)).toString() onValueChanged: { - let decimals = !!backendHandler.ethTokenEntry.item ? backendHandler.ethTokenEntry.item.decimals: 18 - let ethFiatValue = !!backendHandler.ethTokenEntry.item ? backendHandler.ethTokenEntry.item.marketDetails.currencyPrice.amount: 1 + let decimals = !!handler.ethTokenEntry.item ? handler.ethTokenEntry.item.decimals: 18 + let ethFiatValue = !!handler.ethTokenEntry.item ? handler.ethTokenEntry.item.marketDetails.currencyPrice.amount: 1 let totalFees = SQUtils.AmountsArithmetic.div(SQUtils.AmountsArithmetic.fromString(value), SQUtils.AmountsArithmetic.fromNumber(1, decimals)) let totalFeesInFiat = root.fnFormatCurrencyAmount(ethFiatValue*totalFees, root.currentCurrency).toString() simpleSendModal.estimatedCryptoFees = root.fnFormatCurrencyAmount(totalFees.toString(), Constants.ethToken) @@ -393,8 +429,8 @@ QtObject { readonly property var estimatedTimeAggregator: FunctionAggregator { - model: !!backendHandler.fetchedPathModel ? - backendHandler.fetchedPathModel: null + model: !!handler.fetchedPathModel ? + handler.fetchedPathModel: null initialValue: Constants.TransactionEstimatedTime.Unknown roleName: "estimatedTime" @@ -417,6 +453,13 @@ QtObject { value: Constants.ethToken } + readonly property var selectedCollectibleEntry: ModelEntry { + sourceModel: simpleSendModal.isCollectibleSelected ? + root.collectiblesBySymbolModel: null + value: simpleSendModal.selectedTokenKey + key: "symbol" + } + Component.onCompleted: { root.ensNameResolved.connect(ensNameResolved) root.transactionStoreNew.suggestedRoutesReady.connect(routesFetched) @@ -434,6 +477,69 @@ QtObject { simpleSendModal.routerErrorDetails = "" } } + + SignSendAdaptor { + id: signSendAdaptor + accountKey: simpleSendModal.selectedAccountAddress + accountsModel: root.walletAccountsModel + chainId: simpleSendModal.selectedChainId + networksModel: root.flatNetworksModel + tokenKey: simpleSendModal.selectedTokenKey + tokenBySymbolModel: root.tokenBySymbolModel + selectedAmountInBaseUnit: simpleSendModal.selectedAmountInBaseUnit + } + + Component { + id: sendSignModalCmp + SendSignModal { + closePolicy: Popup.CloseOnEscape + destroyOnClose: true + onClosed: root.fnResetDetailedCollectible() + // Unused + formatBigNumber: function(number, symbol, noSymbolOption) {} + + tokenSymbol: !!signSendAdaptor.selectedAsset && + !!signSendAdaptor.selectedAsset.symbol ? + signSendAdaptor.selectedAsset.symbol: "" + tokenAmount: signSendAdaptor.selectedAmount + tokenContractAddress: signSendAdaptor.selectedAssetContractAddress + + accountName: signSendAdaptor.selectedAccount.name + accountAddress: signSendAdaptor.selectedAccount.address + accountEmoji: signSendAdaptor.selectedAccount.emoji + accountColor: Utils.getColorForId(signSendAdaptor.selectedAccount.colorId) + + recipientAddress: simpleSendModal.selectedRecipientAddress + + networkShortName: signSendAdaptor.selectedNetwork.shortName + networkName: signSendAdaptor.selectedNetwork.chainName + networkIconPath: Theme.svg(signSendAdaptor.selectedNetwork.iconUrl) + networkBlockExplorerUrl: signSendAdaptor.selectedNetwork.blockExplorerURL + + fiatFees: simpleSendModal.estimatedFiatFees + cryptoFees: simpleSendModal.estimatedCryptoFees + estimatedTime: simpleSendModal.estimatedTime + + loginType: root.loginType + + isCollectible: simpleSendModal.isCollectibleSelected + isCollectibleLoading: root.isDetailedCollectibleLoading + collectibleContractAddress: root.detailedCollectible.contractAddress + collectibleTokenId: root.detailedCollectible.tokenId + collectibleName: root.detailedCollectible.name + collectibleBackgroundColor: root.detailedCollectible.backgroundColor + collectibleIsMetadataValid: root.detailedCollectible.isMetadataValid + collectibleMediaUrl: root.detailedCollectible.mediaUrl + collectibleMediaType: root.detailedCollectible.mediaType + collectibleFallbackImageUrl: root.detailedCollectible.imageUrl + + fnGetOpenSeaExplorerUrl: root.fnGetOpenSeaUrl + + onAccepted: { + root.transactionStoreNew.authenticateAndTransfer(handler.uuid, simpleSendModal.selectedAccountAddress) + } + } + } } } }