status-desktop/storybook/pages/SimpleSendModalPage.qml
Cuteivist 28690379e1
feat: Simple send modal recipient view (#17096)
* feat: Simple send modal recipient view

* feat: Handle duplicate entries in recent recipient view
2025-01-27 17:35:59 +01:00

757 lines
28 KiB
QML

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import SortFilterProxyModel 0.2
import StatusQ 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Backpressure 0.1
import Models 1.0
import Storybook 1.0
import AppLayouts.Wallet.popups.simpleSend 1.0
import AppLayouts.Wallet.stores 1.0
import AppLayouts.Wallet.adaptors 1.0
import utils 1.0
SplitView {
id: root
orientation: Qt.Horizontal
QtObject {
id: d
readonly property SortFilterProxyModel filteredNetworksModel: SortFilterProxyModel {
sourceModel: NetworksModel.flatNetworks
filters: ValueFilter { roleName: "isTest"; value: testNetworksCheckbox.checked }
}
readonly property WalletAssetsStore walletAssetStore: WalletAssetsStore {
assetsWithFilteredBalances: groupedAccountsAssetsModel
walletTokensStore: TokensStore {
plainTokensBySymbolModel: TokensBySymbolModel{}
getDisplayAssetsBelowBalanceThresholdDisplayAmount: () => 0
}
}
readonly property var walletAccountsModel: WalletAccountsModel{}
function getCurrencyAmount(amount, symbol) {
return ({
amount: amount,
symbol: symbol ? symbol.toUpperCase() : root.currentCurrency,
displayDecimals: 2,
stripTrailingZeroes: false
})
}
readonly property var savedAddressesModel: ListModel {
Component.onCompleted: {
for (let i = 0; i < 10; i++)
append({
name: "some saved addr name " + i,
ens: "",
address: "0x2B748A02e06B159C7C3E98F5064577B96E55A7b4",
})
append({
name: "some saved ENS name ",
ens: "me@status.eth",
address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4",
})
}
}
property var setFees: Backpressure.debounce(root, 1500, function () {
simpleSend.routesLoading = false
simpleSend.estimatedTime = "~60s"
simpleSend.estimatedFiatFees = "1.45 EUR"
simpleSend.estimatedCryptoFees = "0.0007 ETH"
simpleSend.routerErrorCode = Constants.routerErrorCodes.router.errNotEnoughNativeBalance
simpleSend.routerError = qsTr("Not enough ETH to pay gas fees")
simpleSend.routerErrorDetails = ""
})
function formatCurrencyAmount(amount, symbol, options = null, locale = null) {
if (isNaN(amount)) {
return "N/A"
}
var currencyAmount = d.getCurrencyAmount(amount, symbol)
return LocaleUtils.currencyAmountToLocaleString(currencyAmount, options, locale)
}
function resetRouterValues() {
simpleSend.estimatedCryptoFees = ""
simpleSend.estimatedFiatFees = ""
simpleSend.estimatedTime = ""
simpleSend.routerErrorCode = ""
simpleSend.routerError = ""
simpleSend.routerErrorDetails = ""
}
}
PopupBackground {
id: popupBg
SplitView.fillWidth: true
SplitView.fillHeight: true
Button {
id: reopenButton
anchors.centerIn: parent
text: "Reopen"
enabled: !simpleSend.visible
onClicked: simpleSend.open()
}
Component.onCompleted: simpleSend.open()
}
SimpleSendModal {
id: simpleSend
visible: true
modal: false
closePolicy: Popup.CloseOnEscape
interactive: interactiveCheckbox.checked
displayOnlyAssets: displayOnlyAssetsCheckbox.checked
transferOwnership: transferOwnershipCheckbox.checked
accountsModel: accountsSelectorAdaptor.processedWalletAccounts
assetsModel: assetsSelectorViewAdaptor.outputAssetsModel
flatCollectiblesModel: collectiblesSelectionAdaptor.filteredFlatModel
collectiblesModel: collectiblesSelectionAdaptor.model
networksModel: d.filteredNetworksModel
recipientsModel: recipientViewAdaptor.recipientsModel
recipientsFilterModel: recipientViewAdaptor.recipientsFilterModel
currentCurrency: "USD"
fnFormatCurrencyAmount: d.formatCurrencyAmount
fnResolveENS: Backpressure.debounce(root, 500, function (ensName, uuid) {
if (!!ensName && ensName.endsWith(".eth")) {
// return some valid address
simpleSend.ensNameResolved(ensName, "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4", uuid)
} else {
simpleSend.ensNameResolved(ensName, "", uuid) // invalid
}
})
onFormChanged: {
d.resetRouterValues()
if(allValuesFilledCorrectly) {
console.log("Fetch fees...")
routesLoading = true
d.setFees()
}
}
onReviewSendClicked: console.log("Review send clicked")
onLaunchBuyFlow: console.log("launch buy flow clicked")
Binding on selectedAccountAddress {
value: accountsCombobox.currentValue ?? ""
}
Binding on selectedChainId {
value: networksCombobox.currentValue ?? 0
}
Binding on selectedTokenKey {
value: tokensCombobox.currentValue ?? ""
}
}
RecipientViewAdaptor {
id: recipientViewAdaptor
savedAddressesModel: d.savedAddressesModel
accountsModel: d.walletAccountsModel
recentRecipientsModel: WalletTransactionsModel{}
selectedSenderAddress: simpleSend.selectedAccountAddress
selectedRecipientType: simpleSend.selectedRecipientType
searchPattern: simpleSend.recipientSearchPattern
}
WalletAccountsSelectorAdaptor {
id: accountsSelectorAdaptor
accounts: d.walletAccountsModel
assetsModel: GroupedAccountsAssetsModel {}
tokensBySymbolModel: d.walletAssetStore.walletTokensStore.plainTokensBySymbolModel
filteredFlatNetworksModel: d.filteredNetworksModel
selectedTokenKey: simpleSend.selectedTokenKey
selectedNetworkChainId: simpleSend.selectedChainId
fnFormatCurrencyAmountFromBigInt: function(balance, symbol, decimals, options = null) {
let bigIntBalance = AmountsArithmetic.fromString(balance)
let decimalBalance = AmountsArithmetic.toNumber(bigIntBalance, decimals)
return d.formatCurrencyAmount(decimalBalance, symbol, options)
}
}
TokenSelectorViewAdaptor {
id: assetsSelectorViewAdaptor
assetsModel: d.walletAssetStore.groupedAccountAssetsModel
flatNetworksModel: NetworksModel.flatNetworks
currentCurrency: "USD"
showCommunityAssets: true
accountAddress: simpleSend.selectedAccountAddress
enabledChainIds: [simpleSend.selectedChainId]
}
CollectiblesSelectionAdaptor {
id: collectiblesSelectionAdaptor
accountKey: simpleSend.selectedAccountAddress
enabledChainIds: [simpleSend.selectedChainId]
networksModel: d.filteredNetworksModel
collectiblesModel: collectiblesBySymbolModel
filterCommunityOwnerAndMasterTokens: true
}
ListModel {
id: collectiblesBySymbolModel
readonly property var data: [
// collection 2
{
tokenId: "id_3",
symbol: "abc",
chainId: NetworksModel.mainnetChainId,
name: "Multi-seq NFT 1",
contractAddress: "contract_2",
collectionName: "Multi-sequencer Test NFT",
collectionUid: "collection_2",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress1,
balance: 1,
txTimestamp: 1714059810
}
],
imageUrl: Constants.tokenIcon("ETH", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "",
communityName: "",
communityImage: Qt.resolvedUrl(""),
tokenType: Constants.TokenType.ERC721
},
{
tokenId: "id_4",
symbol: "def",
chainId: NetworksModel.mainnetChainId,
name: "Multi-seq NFT 2",
contractAddress: "contract_2",
collectionName: "Multi-sequencer Test NFT",
collectionUid: "collection_2",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress1,
balance: 1,
txTimestamp: 1714059811
}
],
imageUrl: Constants.tokenIcon("ETH", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "",
communityName: "",
communityImage: Qt.resolvedUrl(""),
tokenType: Constants.TokenType.ERC721
},
{
tokenId: "id_5",
symbol: "ghi",
chainId: NetworksModel.mainnetChainId,
name: "Multi-seq NFT 3",
contractAddress: "contract_2",
collectionName: "Multi-sequencer Test NFT",
collectionUid: "collection_2",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress1,
balance: 1,
txTimestamp: 1714059899
}
],
imageUrl: Constants.tokenIcon("ETH", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "",
communityName: "",
communityImage: Qt.resolvedUrl(""),
tokenType: Constants.TokenType.ERC721
},
// collection 1
{
tokenId: "id_1",
symbol: "jkl",
chainId: NetworksModel.mainnetChainId,
name: "Genesis",
contractAddress: "contract_1",
collectionName: "ERC-1155 Faucet",
collectionUid: "collection_1",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress1,
balance: 23,
txTimestamp: 1714059862
},
{
accountAddress: d.walletAccountsModel.accountAddress2,
balance: 29,
txTimestamp: 1714054862
}
],
imageUrl: Constants.tokenIcon("DAI", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "",
communityName: "",
communityImage: Qt.resolvedUrl(""),
tokenType: Constants.TokenType.ERC1155
},
{
tokenId: "id_2",
symbol: "mno",
chainId: NetworksModel.mainnetChainId,
name: "QAERC1155",
contractAddress: "contract_1",
collectionName: "ERC-1155 Faucet",
collectionUid: "collection_1",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress1,
balance: 500,
txTimestamp: 1714059864
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "",
communityName: "",
communityImage: Qt.resolvedUrl(""),
tokenType: Constants.TokenType.ERC1155
},
// collection 3, community token
{
tokenId: "id_6",
symbol: "pqr",
chainId: NetworksModel.optChainId,
name: "My Token",
contractAddress: "contract_3",
collectionName: "My Token",
collectionUid: "collection_3",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress1,
balance: 1,
txTimestamp: 1714059899
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_1",
communityName: "My community",
communityImage: Constants.tokenIcon("KIN", false),
tokenType: Constants.TokenType.ERC721
},
{
tokenId: "id_7",
symbol: "stu",
chainId: NetworksModel.optChainId,
name: "My Token",
contractAddress: "contract_3",
collectionName: "My Token",
collectionUid: "collection_3",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress1,
balance: 1,
txTimestamp: 1714059899
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_1",
communityName: "My community",
communityImage: Constants.tokenIcon("KIN", false),
tokenType: Constants.TokenType.ERC721
},
{
tokenId: "id_8",
symbol: "vwx",
chainId: NetworksModel.optChainId,
name: "My Token",
contractAddress: "contract_3",
collectionName: "My Token",
collectionUid: "collection_3",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress2,
balance: 1,
txTimestamp: 1714059999
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_1",
communityName: "My community",
communityImage: Constants.tokenIcon("KIN", false),
tokenType: Constants.TokenType.ERC721
},
{
tokenId: "id_9",
symbol: "yz1",
chainId: NetworksModel.optChainId,
name: "My Other Token",
contractAddress: "contract_4",
collectionName: "My Other Token",
collectionUid: "collection_4",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress1,
balance: 1,
txTimestamp: 1714059991
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_1",
communityName: "My community",
communityImage: Constants.tokenIcon("KIN", false),
tokenType: Constants.TokenType.ERC721
},
{
tokenId: "id_10",
symbol: "234",
chainId: NetworksModel.arbChainId,
name: "My Community 2 Token",
contractAddress: "contract_5",
collectionName: "My Community 2 Token",
collectionUid: "collection_5",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress1,
balance: 1,
txTimestamp: 1714059777
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_2",
communityName: "My community 2",
communityImage: Constants.tokenIcon("ICOS", false),
tokenType: Constants.TokenType.ERC721
},
{
tokenId: "id_11",
symbol: "567",
chainId: NetworksModel.arbChainId,
name: "My Community 2 Token",
contractAddress: "contract_5",
collectionName: "My Community 2 Token",
collectionUid: "collection_5",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress1,
balance: 1,
txTimestamp: 1714059778
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_2",
communityName: "My community 2",
communityImage: Constants.tokenIcon("ICOS", false),
tokenType: Constants.TokenType.ERC721
},
{
tokenId: "id_11",
symbol: "8910",
chainId: NetworksModel.arbChainId,
name: "My Community 2 Token",
contractAddress: "contract_5",
collectionName: "My Community 2 Token",
collectionUid: "collection_5",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress2,
balance: 1,
txTimestamp: 1714059779
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_2",
communityName: "My community 2",
communityImage: Constants.tokenIcon("ICOS", false),
tokenType: Constants.TokenType.ERC721
},
{
tokenId: "id_12",
symbol: "111213",
chainId: NetworksModel.arbChainId,
name: "My Community 2 Token",
contractAddress: "contract_5",
collectionName: "My Community 2 Token",
collectionUid: "collection_5",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress3,
balance: 1,
txTimestamp: 1714059779
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_2",
communityName: "My community 2",
communityImage: Constants.tokenIcon("ICOS", false),
tokenType: Constants.TokenType.ERC721
},
{
tokenId: "id_13",
symbol: "141516",
chainId: NetworksModel.arbChainId,
name: "My Community 2 Token",
contractAddress: "contract_5",
collectionName: "My Community 2 Token",
collectionUid: "collection_5",
ownership: [
{
accountAddress: d.walletAccountsModel.accountAddress3,
balance: 1,
txTimestamp: 1714059788
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_2",
communityName: "My community 2",
communityImage: Constants.tokenIcon("ICOS", false),
tokenType: Constants.TokenType.ERC721
}
]
Component.onCompleted: {
append(data)
}
}
Pane {
SplitView.minimumHeight: 100
SplitView.minimumWidth: 300
SplitView.maximumWidth: 380
ColumnLayout {
spacing: 20
CheckBox {
id: interactiveCheckbox
text: "Is interactive"
checked: true
}
CheckBox {
id: transferOwnershipCheckbox
text: "transfer ownership"
checked: false
}
CheckBox {
id: displayOnlyAssetsCheckbox
text: "displayOnlyAssets"
}
Text {
text: "Select an accounts"
}
ComboBox {
id: accountsCombobox
model: SortFilterProxyModel {
sourceModel: d.walletAccountsModel
filters: ValueFilter {
roleName: "walletType"
value: Constants.watchWalletType
inverted: true
}
}
textRole: "name"
valueRole: "address"
currentIndex: 0
}
CheckBox {
id: testNetworksCheckbox
text: "are test networks enabled"
}
Text {
text: "Select a network"
}
ComboBox {
id: networksCombobox
model: d.filteredNetworksModel
textRole: "chainName"
valueRole: "chainId"
currentIndex: 0
}
Text {
text: "Select a token"
}
ComboBox {
id: tokensCombobox
Layout.preferredWidth: 200
model: ConcatModel {
sources: [
SourceModel {
model: ObjectProxyModel {
sourceModel: d.walletAssetStore.walletTokensStore.plainTokensBySymbolModel
delegate: SortFilterProxyModel {
readonly property var addressPerChain: this
sourceModel: LeftJoinModel {
leftModel: model.addressPerChain
rightModel: d.filteredNetworksModel
joinRole: "chainId"
}
filters: ValueFilter {
roleName: "isTest"
value: testNetworksCheckbox.checked
}
}
expectedRoles: "addressPerChain"
exposedRoles: "addressPerChain"
}
markerRoleValue: "first_model"
},
SourceModel {
model: RolesRenamingModel {
sourceModel: collectiblesBySymbolModel
mapping: [
RoleRename {
from: "symbol"
to: "key"
}
]
}
markerRoleValue: "second_model"
}
]
markerRoleName: "which_model"
expectedRoles: ["key", "name", "addressPerChain", "chainId", "ownership", "type", "tokenType", "addressPerChain"]
}
delegate: ItemDelegate {
contentItem: RowLayout {
Text {
text: model.name
}
StatusIcon {
icon: {
const iconUrl = ModelUtils.getByKey(NetworksModel.flatNetworks, "chainId", model.chainId, "iconUrl")
if(!!iconUrl)
return Theme.svg(iconUrl)
else return ""
}
}
}
onClicked: {
let tokenType = model.type ?? model.tokenType
if (tokenType === Constants.TokenType.ERC721) {
simpleSend.sendType = Constants.SendType.ERC721Transfer
simpleSend.selectedChainId = model.chainId
const firstAccountOwningSelectedCollectible = ModelUtils.get(model.ownership, 0, "accountAddress")
if(!!firstAccountOwningSelectedCollectible)
simpleSend.selectedAccountAddress = firstAccountOwningSelectedCollectible
}
else if (tokenType === Constants.TokenType.ERC1155) {
simpleSend.sendType = Constants.SendType.ERC1155Transfer
simpleSend.selectedChainId = model.chainId
const firstAccountOwningSelectedCollectible = ModelUtils.get(model.ownership, 0, "accountAddress")
if(!!firstAccountOwningSelectedCollectible)
simpleSend.selectedAccountAddress = firstAccountOwningSelectedCollectible
}
else {
let selectedChainId = ModelUtils.getByKey(model.addressPerChain, "layer", "1", "chainId")
if (!selectedChainId) {
selectedChainId = ModelUtils.get(model.addressPerChain, 0, "chainId")
}
simpleSend.selectedChainId = selectedChainId
simpleSend.sendType = Constants.SendType.Transfer
}
}
highlighted: tokensCombobox.highlightedIndex === index
}
textRole: "name"
valueRole: "key"
}
RowLayout {
Layout.fillWidth: true
TextField {
id: amountInput
Layout.preferredWidth: 200
Layout.preferredHeight: 50
validator: RegularExpressionValidator {
regularExpression: /^\d*\.?\d*$/
}
}
Button {
text: "update raw value in modal"
onClicked: simpleSend.selectedRawAmount = amountInput.text
}
}
Text {
text: "Select a recipient"
}
RowLayout {
Layout.fillWidth: true
TextField {
id: recipientInput
Layout.preferredWidth: 200
Layout.preferredHeight: 50
}
Button {
text: "update in modal"
onClicked: simpleSend.selectedRecipientAddress = recipientInput.text
}
}
Text {
text: "account selected is: \n"
+ simpleSend.selectedAccountAddress
}
Text {
text: "network selected is: " + simpleSend.selectedChainId
}
Text {
text: "token selected is: " + simpleSend.selectedTokenKey
}
Text {
text: "raw amount entered is: " + simpleSend.selectedRawAmount
}
Text {
text: "selected recipient is: \n" + simpleSend.selectedRecipientAddress
}
}
}
}
// category: Popups