feat(wallet) user can repeat a Send transaction from activity view

Enable user action to repeat a Send transaction from the activity view
(HistoryView) and details view (TransactionDetailView).

Extend AppMain send modal entry and SendModal API to allow for selecting
all the required parameters for repeating a transaction.

Optimize update of start timestamp for activity filter only when user
attempts to open the filter panel.

Closes #12122
This commit is contained in:
Stefan 2023-09-27 16:41:55 +03:00 committed by Stefan Dunca
parent 41672271dc
commit e805a9bf26
12 changed files with 214 additions and 27 deletions

View File

@ -343,8 +343,6 @@ QtObject:
self.allAddressesSelected = allAddressesSelected
self.status.setIsFilterDirty(true)
self.updateStartTimestamp()
proc setFilterAddressesJson*(self: Controller, jsonArray: string, allAddressesSelected: bool) {.slot.} =
let addressesJson = parseJson(jsonArray)
if addressesJson.kind != JArray:
@ -393,9 +391,6 @@ QtObject:
if res.result.getBool():
self.status.setLoadingRecipients(false)
proc updateFilterBase(self: Controller) {.slot.} =
self.updateStartTimestamp()
proc getStatus*(self: Controller): QVariant {.slot.} =
return newQVariant(self.status)

View File

@ -21,10 +21,6 @@ Column {
spacing: 12
Component.onCompleted: {
activityFilterStore.updateFilterBase()
}
function resetView() {
activityFilterMenu.resetView()
}
@ -43,7 +39,10 @@ Column {
border.color: Theme.palette.directColor8
type: StatusRoundButton.Type.Tertiary
icon.color: Theme.palette.primaryColor1
onClicked: activityFilterMenu.popup(x, y + height + 4)
onClicked: {
activityFilterStore.updateStartTimestamp()
activityFilterMenu.popup(x, y + height + 4)
}
}
ActivityFilterTagItem {

View File

@ -247,8 +247,8 @@ QtObject {
activityController.updateFilter()
}
function updateFilterBase() {
activityController.updateFilterBase()
function updateStartTimestamp() {
activityController.updateStartTimestamp()
}
function applyAllFilters() {

View File

@ -5,6 +5,7 @@ import QtQuick 2.13
import utils 1.0
import SortFilterProxyModel 0.2
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
QtObject {
id: root
@ -237,6 +238,59 @@ QtObject {
return name
}
enum LookupType {
Account = 0,
SavedAddress = 1
}
// Returns object of type {type: null, object: null} or null if lookup didn't find anything
function lookupAddressObject(address) {
let res = null
let acc = ModelUtils.getByKey(root.accounts, "address", address)
if (acc) {
res = {type: RootStore.LookupType.Account, object: acc}
} else {
let sa = ModelUtils.getByKey(walletSectionSavedAddresses.model, "address", address)
if (sa) {
res = {type: RootStore.LookupType.SavedAddress, object: sa}
}
}
return res
}
function getAssetForSendTx(tx) {
if (tx.isNFT) {
return {
uid: tx.tokenID,
chainId: tx.chainId,
name: tx.nftName,
imageUrl: tx.nftImageUrl,
collectionUid: "",
collectionName: ""
}
} else {
return tx.symbol
}
}
function isTxRepeatable(tx) {
if (tx.txType !== Constants.TransactionType.Send)
return false
let res = root.lookupAddressObject(tx.sender)
if (!res || res.type !== RootStore.LookupType.Account || res.object.walletType == Constants.watchWalletType)
return false
if (tx.isNFT) {
// TODO #12275: check if account owns enough NFT
} else {
// TODO #12275: Check if account owns enough tokens
}
return true
}
function isOwnedAccount(address) {
return walletSectionAccounts.isOwnedAccount(address)
}

View File

@ -145,6 +145,7 @@ Item {
id: historyView
overview: RootStore.overview
showAllAccounts: root.showAllAccounts
sendModal: root.sendModal
onLaunchTransactionDetail: function (entry, entryIndex) {
transactionDetailView.transactionIndex = entryIndex
transactionDetailView.transaction = entry

View File

@ -13,7 +13,7 @@ import StatusQ.Popups 0.1
import shared.controls 1.0
import shared.panels 1.0
import utils 1.0
import shared.stores 1.0
import shared.popups.send 1.0
import "../controls"
import "../popups"
@ -714,7 +714,7 @@ Item {
}
}
}
Separator {
width: progressBlock.width
}
@ -728,10 +728,29 @@ Item {
Layout.preferredHeight: copyDetailsButton.height
text: qsTr("Repeat transaction")
size: StatusButton.Small
visible: root.isTransactionValid && !root.overview.isWatchOnlyAccount && d.transactionType === TransactionDelegate.Send
property alias tx: root.transaction
visible: {
if (!root.isTransactionValid || root.overview.isWatchOnlyAccount)
return false
return WalletStores.RootStore.isTxRepeatable(tx)
}
onClicked: {
root.sendModal.open(root.transaction.to)
// TODO handle other types
let asset = WalletStores.RootStore.getAssetForSendTx(tx)
let req = Helpers.lookupAddressesForSendModal(tx.sender, tx.recipient, asset, tx.isNFT, tx.amount)
root.sendModal.preSelectedAccount = req.preSelectedAccount
root.sendModal.preSelectedRecipient = req.preSelectedRecipient
root.sendModal.preSelectedRecipientType = req.preSelectedRecipientType
root.sendModal.preSelectedHolding = req.preSelectedHolding
root.sendModal.preSelectedHoldingID = req.preSelectedHoldingID
root.sendModal.preSelectedHoldingType = req.preSelectedHoldingType
root.sendModal.preSelectedSendType = req.preSelectedSendType
root.sendModal.preDefinedAmountToSend = req.preDefinedAmountToSend
root.sendModal.onlyAssets = false
root.sendModal.open()
}
}
StatusButton {

View File

@ -24,6 +24,7 @@ import shared.popups.keycard 1.0
import shared.status 1.0
import shared.stores 1.0
import shared.popups.send 1.0
import shared.popups.send.views 1.0
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
@ -1360,16 +1361,20 @@ Item {
this.active = true
this.item.open()
}
function closed() {
// this.sourceComponent = undefined // kill an opened instance
this.active = false
}
property var preSelectedAccount
property var preSelectedRecipient
property int preSelectedRecipientType
property var preSelectedHolding
property string preSelectedHoldingID
property int preSelectedHoldingType
property int preSelectedSendType: Constants.SendType.Unknown
property string preDefinedAmountToSend
property bool onlyAssets: false
sourceComponent: SendModal {
@ -1381,12 +1386,18 @@ Item {
sendModal.preSelectedHoldingType = Constants.HoldingType.Unknown
sendModal.preSelectedHolding = undefined
sendModal.preSelectedAccount = undefined
sendModal.preSelectedRecipient = undefined
sendModal.preDefinedAmountToSend = ""
}
}
onLoaded: {
if (!!sendModal.preSelectedAccount) {
item.preSelectedAccount = sendModal.preSelectedAccount
}
if (!!sendModal.preSelectedRecipient) {
item.preSelectedRecipient = sendModal.preSelectedRecipient
item.preSelectedRecipientType = sendModal.preSelectedRecipientType
}
if(sendModal.preSelectedSendType !== Constants.SendType.Unknown) {
item.preSelectedSendType = sendModal.preSelectedSendType
}
@ -1397,6 +1408,9 @@ Item {
if(!!preSelectedHolding) {
item.preSelectedHolding = preSelectedHolding
}
if(preDefinedAmountToSend != "") {
item.preDefinedAmountToSend = preDefinedAmountToSend
}
}
}

View File

@ -0,0 +1,70 @@
pragma Singleton
import QtQuick 2.15
import QtQml 2.15
import utils 1.0
import shared.stores 1.0
import shared.stores.send 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStores
import StatusQ.Core 0.1
import "./panels"
import "./controls"
import "./views"
QtObject {
id: root
function createSendModalRequirements() {
return {
preSelectedAccount: null,
preSelectedRecipientType: TabAddressSelectorView.Type.Address,
preSelectedRecipient: null,
preSelectedHoldingType: 0,
preSelectedHolding: null,
preSelectedHoldingID: "",
preDefinedAmountToSend: "",
preSelectedSendType: Constants.SendType.Transfer
}
}
// \c token is an collectible object in case of \c isCollectible == true otherwise a token code (e.g. "ETH")
function lookupAddressesForSendModal(senderAddress, recipientAddress, token, isCollectible, amount) {
let req = createSendModalRequirements()
req.preSelectedSendType = Constants.SendType.Transfer
let senderAccount = null
let resolvedAcc = WalletStores.RootStore.lookupAddressObject(senderAddress)
if (resolvedAcc && resolvedAcc.type == WalletStores.RootStore.LookupType.Account) {
req.preSelectedAccount = resolvedAcc.object
}
let res = WalletStores.RootStore.lookupAddressObject(recipientAddress)
if (res) {
if (res.type == WalletStores.RootStore.LookupType.Account) {
req.preSelectedRecipientType = TabAddressSelectorView.Type.Account
req.preSelectedRecipient = res.object
} else if (res.type == WalletStores.RootStore.LookupType.SavedAddress) {
req.preSelectedRecipientType = TabAddressSelectorView.Type.SavedAddress
req.preSelectedRecipient = res.object
}
} else {
req.preSelectedRecipientType = TabAddressSelectorView.Type.Address
req.preSelectedRecipient = recipientAddress
}
if (isCollectible) {
req.preSelectedHoldingType = Constants.HoldingType.Collectible
req.preSelectedHolding = token
} else {
req.preSelectedHoldingType = Constants.HoldingType.Asset
req.preSelectedHoldingID = token
}
req.preDefinedAmountToSend = LocaleUtils.numberToLocaleString(amount)
return req
}
}

View File

@ -24,9 +24,14 @@ import "./views"
StatusDialog {
id: popup
property string preSelectedRecipient
// expected content depends on the preSelectedRecipientType value.
// If type Address this must be a string else it expects an object. See RecipientView.selectedRecipientType
property var preSelectedRecipient
property int preSelectedRecipientType: TabAddressSelectorView.Type.Address
property string preDefinedAmountToSend
// requires to have assigned an item from assets model
property var preSelectedHolding
// token symbol
property string preSelectedHoldingID
property int preSelectedHoldingType
property int preSelectedSendType
@ -156,8 +161,12 @@ StatusDialog {
}
if(!!popup.preSelectedRecipient) {
recipientLoader.selectedRecipientType = TabAddressSelectorView.Type.Address
recipientLoader.selectedRecipient = {address: popup.preSelectedRecipient}
recipientLoader.selectedRecipientType = popup.preSelectedRecipientType
if (popup.preSelectedRecipientType == TabAddressSelectorView.Type.Address) {
recipientLoader.selectedRecipient = {address: popup.preSelectedRecipient}
} else {
recipientLoader.selectedRecipient = popup.preSelectedRecipient
}
}
if(d.isBridgeTx) {

View File

@ -1 +1,2 @@
SendModal 1.0 SendModal.qml
singleton Helpers 1.0 Helpers.qml

View File

@ -37,7 +37,7 @@ Loader {
if(!!root.selectedRecipient && root.selectedRecipientType !== TabAddressSelectorView.Type.None) {
let preferredChainIds = []
switch(root.selectedRecipientType) {
case TabAddressSelectorView.Type.Account: {
case TabAddressSelectorView.Type.Account: {
root.addressText = root.selectedRecipient.address
preferredChainIds = root.selectedRecipient.preferredSharingChainIds
break
@ -105,8 +105,10 @@ Loader {
}
}
sourceComponent: root.selectedRecipientType === TabAddressSelectorView.Type.SavedAddress ? savedAddressRecipient:
root.selectedRecipientType === TabAddressSelectorView.Type.Account ? myAccountRecipient : addressRecipient
sourceComponent: root.selectedRecipientType === TabAddressSelectorView.Type.SavedAddress
? savedAddressRecipient
: root.selectedRecipientType === TabAddressSelectorView.Type.Account
? myAccountRecipient : addressRecipient
Component {
id: savedAddressRecipient

View File

@ -15,6 +15,7 @@ import utils 1.0
import "../panels"
import "../popups"
import "../popups/send"
import "../stores"
import "../controls"
@ -28,6 +29,7 @@ ColumnLayout {
property var overview
property bool showAllAccounts: false
property var sendModal
signal launchTransactionDetail(var transaction, int entryIndex)
@ -303,7 +305,6 @@ ColumnLayout {
delegateMenu.transactionDelegate = delegate
delegateMenu.transaction = data
repeatTransactionAction.enabled = !overview.isWatchOnlyAccount && delegate.modelData.txType === TransactionDelegate.Send
popup(delegate, mouse.x, mouse.y)
}
@ -314,13 +315,35 @@ ColumnLayout {
StatusAction {
id: repeatTransactionAction
text: qsTr("Repeat transaction")
enabled: false
icon.name: "rotate"
property alias tx: delegateMenu.transaction
enabled: {
if (!overview.isWatchOnlyAccount && !tx)
return false
return WalletStores.RootStore.isTxRepeatable(tx)
}
onTriggered: {
if (!delegateMenu.transaction)
if (!tx)
return
root.sendModal.open(delegateMenu.transaction.to)
let asset = WalletStores.RootStore.getAssetForSendTx(tx)
let req = Helpers.lookupAddressesForSendModal(tx.sender, tx.recipient, asset, tx.isNFT, tx.amount)
root.sendModal.preSelectedAccount = req.preSelectedAccount
root.sendModal.preSelectedRecipient = req.preSelectedRecipient
root.sendModal.preSelectedRecipientType = req.preSelectedRecipientType
root.sendModal.preSelectedHolding = req.preSelectedHolding
root.sendModal.preSelectedHoldingID = req.preSelectedHoldingID
root.sendModal.preSelectedHoldingType = req.preSelectedHoldingType
root.sendModal.preSelectedSendType = req.preSelectedSendType
root.sendModal.preDefinedAmountToSend = req.preDefinedAmountToSend
root.sendModal.onlyAssets = false
root.sendModal.open()
}
}
StatusSuccessAction {