wip: request payment modal

todo: remove unused imports
todo: finish handling amount input
This commit is contained in:
Emil Sawicki 2024-10-28 14:56:34 +01:00
parent 3e9e8bfe07
commit 0b87e072cc
10 changed files with 561 additions and 9 deletions

View File

@ -0,0 +1,187 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import SortFilterProxyModel 0.2
import QtTest 1.15
import StatusQ 0.1
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Backpressure 0.1
import StatusQ.Core.Utils 0.1
import utils 1.0
import Storybook 1.0
import Models 1.0
import mainui 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStores
import AppLayouts.Chat.popups 1.0
import AppLayouts.stores 1.0 as AppLayoutStores
import shared.stores 1.0 as SharedStores
// TODO_ES remove unneeded imports
SplitView {
id: root
Logs { id: logs }
orientation: Qt.Horizontal
ListModel {
id: plainTokensModel
ListElement {
key: "aave"
name: "Aave"
symbol: "AAVE"
image: "https://cryptologos.cc/logos/aave-aave-logo.png"
communityId: ""
}
ListElement {
key: "usdc"
name: "USDC"
symbol: "USDC"
image: ""
communityId: ""
}
ListElement {
key: "hst"
name: "Decision Token"
symbol: "HST"
image: "https://etherscan.io/token/images/horizonstate2_28.png"
communityId: ""
}
}
QtObject {
id: d
readonly property var tokenBySymbolModel: TokensBySymbolModel {}
function launchPopup() {
requestPaymentModalComponent.createObject(root)
}
readonly property var accounts: WalletAccountsModel {}
readonly property SharedStores.CurrenciesStore currencyStore: SharedStores.CurrenciesStore {}
readonly property var flatNetworks: NetworksModel.flatNetworks
readonly property var walletAssetsStore: WalletStores.WalletAssetsStore {
id: thisWalletAssetStore
walletTokensStore: WalletStores.TokensStore {
plainTokensBySymbolModel: TokensBySymbolModel {}
}
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
}
readonly property string selectedAccountAddress: ctrlAccount.currentValue ?? ""
readonly property int selectedNetworkChainId: ctrlSelectedNetworkChainId.currentValue ?? -1
readonly property SharedStores.RequestPaymentStore requestPaymentStore: SharedStores.RequestPaymentStore {
currencyStore: d.currencyStore
flatNetworksModel: d.flatNetworks
processedAssetsModel: d.walletAssetsStore.renamedTokensBySymbolModel
accountsModel: d.accounts
}
}
PopupBackground {
id: popupBg
SplitView.fillWidth: true
SplitView.fillHeight: true
Button {
id: reopenButton
anchors.centerIn: parent
text: "Reopen"
enabled: !requestPaymentModalComponent.visible
onClicked: d.launchPopup()
}
Component.onCompleted: d.launchPopup()
Component {
id: requestPaymentModalComponent
RequestPaymentModal {
id: requestPaymentModal
visible: true
modal: false
closePolicy: Popup.CloseOnEscape
destroyOnClose: true
store: d.requestPaymentStore
Connections {
target: d
function onSelectedNetworkChainIdChanged() {
requestPaymentModal.selectedNetworkChainId = d.selectedNetworkChainId
}
function onSelectedAccountAddressChanged() {
requestPaymentModal.selectedAccountAddress = d.selectedAccountAddress
}
}
Component.onCompleted: {
if (d.selectedNetworkChainId > -1)
requestPaymentModal.selectedNetworkChainId = d.selectedNetworkChainId
if (!!d.selectedAccountAddress)
requestPaymentModal.selectedAccountAddress = d.selectedAccountAddress
}
}
}
}
ScrollView {
id: rightPanel
SplitView.minimumWidth: 300
SplitView.preferredWidth: 300
SplitView.minimumHeight: 300
ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: 10
spacing: 10
Label {
text: "pre-selection:"
}
RowLayout {
Layout.fillWidth: true
Label {
text: "Chain:"
}
ComboBox {
Layout.fillWidth: true
id: ctrlSelectedNetworkChainId
model: d.flatNetworks
textRole: "chainName"
valueRole: "chainId"
displayText: currentIndex === -1 ? "All chains" : currentText
currentIndex: -1 // all chains
}
}
RowLayout {
Layout.fillWidth: true
Label { text: "Account:" }
ComboBox {
Layout.fillWidth: true
id: ctrlAccount
textRole: "name"
valueRole: "address"
displayText: currentText || "----"
model: SortFilterProxyModel {
sourceModel: d.accounts
sorters: RoleSorter { roleName: "position" }
}
currentIndex: -1
}
}
}
}
}
// category: Popups

View File

@ -11,6 +11,7 @@ import shared.stores 1.0 as SharedStores
import StatusQ.Core.Utils 0.1 as SQUtils import StatusQ.Core.Utils 0.1 as SQUtils
import AppLayouts.Wallet.stores 1.0 as WalletStores
import AppLayouts.Chat.stores 1.0 as ChatStores import AppLayouts.Chat.stores 1.0 as ChatStores
SplitView { SplitView {
@ -99,6 +100,8 @@ SplitView {
} }
} }
requestPaymentStore: d.requestPaymentStore
onSendMessage: { onSendMessage: {
logs.logEvent("StatusChatInput::sendMessage", ["MessageWithPk"], [chatInput.getTextWithPublicKeys()]) logs.logEvent("StatusChatInput::sendMessage", ["MessageWithPk"], [chatInput.getTextWithPublicKeys()])
logs.logEvent("StatusChatInput::sendMessage", ["PlainText"], [SQUtils.StringUtils.plainText(chatInput.getTextWithPublicKeys())]) logs.logEvent("StatusChatInput::sendMessage", ["PlainText"], [SQUtils.StringUtils.plainText(chatInput.getTextWithPublicKeys())])
@ -138,6 +141,23 @@ SplitView {
QtObject { QtObject {
id: d id: d
readonly property var walletAssetsStore: WalletStores.WalletAssetsStore {
id: thisWalletAssetStore
walletTokensStore: WalletStores.TokensStore {
plainTokensBySymbolModel: TokensBySymbolModel {}
}
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
}
readonly property SharedStores.RequestPaymentStore requestPaymentStore: SharedStores.RequestPaymentStore {
currencyStore: SharedStores.CurrenciesStore {}
flatNetworksModel: NetworksModel.flatNetworks
processedAssetsModel: d.walletAssetsStore.renamedTokensBySymbolModel
accountsModel: WalletAccountsModel {}
}
property bool linkPreviewsEnabled: linkPreviewSwitch.checked && !askToEnableLinkPreviewSwitch.checked property bool linkPreviewsEnabled: linkPreviewSwitch.checked && !askToEnableLinkPreviewSwitch.checked
onLinkPreviewsEnabledChanged: { onLinkPreviewsEnabledChanged: {
loadLinkPreviews(chatInputLoader.item ? chatInputLoader.item.unformattedText : "") loadLinkPreviews(chatInputLoader.item ? chatInputLoader.item.unformattedText : "")

View File

@ -0,0 +1,8 @@
import QtQuick 2.15
QtObject {
required property CurrenciesStore currencyStore
required property var flatNetworksModel
required property var processedAssetsModel
required property var accountsModel
}

View File

@ -6,5 +6,6 @@ GifStore 1.0 GifStore.qml
NetworkConnectionStore 1.0 NetworkConnectionStore.qml NetworkConnectionStore 1.0 NetworkConnectionStore.qml
PermissionsStore 1.0 PermissionsStore.qml PermissionsStore 1.0 PermissionsStore.qml
ProfileStore 1.0 ProfileStore.qml ProfileStore 1.0 ProfileStore.qml
RequestPaymentStore 1.0 RequestPaymentStore.qml
RootStore 1.0 RootStore.qml RootStore 1.0 RootStore.qml
UtilsStore 1.0 UtilsStore.qml UtilsStore 1.0 UtilsStore.qml

View File

@ -0,0 +1,265 @@
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.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import StatusQ.Components.private 0.1 as SQP
import StatusQ.Components 0.1
import StatusQ.Popups.Dialog 0.1
import AppLayouts.Wallet.controls 1.0
import AppLayouts.Wallet.adaptors 1.0
import shared.popups.send.views 1.0
import shared.controls 1.0
import shared.stores 1.0 as SharedStores
import utils 1.0
StatusDialog {
id: root
required property SharedStores.RequestPaymentStore store
property int selectedNetworkChainId: Constants.chains.mainnetChainId
property string selectedAccountAddress
property string selectedTokenKey: Constants.ethToken
onSelectedTokenKeyChanged: Qt.callLater(d.reevaluateSelectedId)
readonly property string amount: {
if (!d.isSelectedHoldingValidAsset || !d.selectedHolding.marketDetails || !d.selectedHolding.marketDetails.currencyPrice) {
return "0"
}
return amountToSendInput.text
}
objectName: "requestPaymentModal"
implicitWidth: 480
implicitHeight: 470
modal: true
padding: 0
backgroundColor: Theme.palette.statusModal.backgroundColor
title: qsTr("Payment request")
QtObject {
id: d
// FIXME use ModelEntry
property var selectedHolding: SQUtils.ModelUtils.getByKey(holdingSelector.model, "tokensKey", root.selectedTokenKey)
readonly property bool isSelectedHoldingValidAsset: !!selectedHolding
readonly property var adaptor: TokenSelectorViewAdaptor {
assetsModel: root.store.processedAssetsModel
flatNetworksModel: root.flatNetworksModel
currentCurrency: root.store.currencyStore.currentCurrency
showAllTokens: true
}
// FIXME drop after using ModelEntry, shouldn't be needed
function reevaluateSelectedId() {
const entry = SQUtils.ModelUtils.getByKey(holdingSelector.model, "tokensKey", root.selectedTokenKey)
if (entry) {
holdingSelector.setSelection(entry.symbol, entry.iconSource, entry.tokensKey)
} else {
root.selectedTokenKey = ""
holdingSelector.reset()
}
d.selectedHolding = entry
}
}
footer: StatusDialogFooter {
StatusDialogDivider {
anchors.top: parent.top
width: parent.width
}
rightButtons: ObjectModel {
StatusButton {
objectName: "sendButton"
text: qsTr("Add to message")
disabledColor: Theme.palette.directColor8
enabled: amountToSendInput.valid && !amountToSendInput.empty && amountToSendInput.asNumber > 0
interactive: true
onClicked: {
// TODO_ES handle
root.accept()
}
}
}
}
ColumnLayout {
anchors.top: parent.top
anchors.topMargin: Theme.bigPadding
anchors.left: parent.left
anchors.leftMargin: Theme.padding
anchors.right: parent.right
anchors.rightMargin: Theme.padding
spacing: Theme.padding
AmountToSend {
id: amountToSendInput
Layout.fillWidth: true
readonly property bool ready: valid && !empty
readonly property string selectedSymbol: root.selectedTokenKey
// For backward compatibility. To be removed when
// dependent components (NetworkSelector, AmountToReceive)
// are refactored.
readonly property double asNumber: {
if (!valid)
return 0
return parseFloat(text.replace(LocaleUtils.userInputLocale.decimalPoint, "."))
}
readonly property int minSendCryptoDecimals:
!fiatMode ? LocaleUtils.fractionalPartLength(asNumber) : 0
readonly property int minReceiveCryptoDecimals:
!fiatMode ? minSendCryptoDecimals + 1 : 0
readonly property int minSendFiatDecimals:
fiatMode ? LocaleUtils.fractionalPartLength(asNumber) : 0
readonly property int minReceiveFiatDecimals:
fiatMode ? minSendFiatDecimals + 1 : 0
// End of to-be-removed part
multiplierIndex: 9
// !!holdingSelector.selectedItem
// && !!holdingSelector.selectedItem.decimals
// ? holdingSelector.selectedItem.decimals : 0
// price: d.isSelectedHoldingValidAsset
// ? (d.selectedHolding ?
// d.selectedHolding.marketDetails.currencyPrice.amount : 1)
// : 1
price: 1
formatFiat: amount => root.store.currencyStore.formatCurrencyAmount(
amount, root.store.currencyStore.currentCurrency)
formatBalance: amount => root.store.currencyStore.formatCurrencyAmount(
amount, selectedSymbol)
showSeparator: true
onValidChanged: {
}
onAmountChanged: {
}
AssetSelector {
id: holdingSelector
anchors.top: parent.top
anchors.right: parent.right
model: d.adaptor.outputAssetsModel
onSelected: root.selectedTokenKey = key
}
}
StatusBaseText {
text: qsTr("Into")
color: Theme.palette.directColor5
font.weight: Font.Medium
}
AccountSelector {
id: accountSelector
model: root.store.accountsModel
Layout.fillWidth: true
Layout.preferredHeight: 64
size: StatusComboBox.Size.Large
control.background: SQP.StatusComboboxBackground {
active: accountSelector.control.down || accountSelector.control.hovered
}
popup.verticalPadding: 0
popup.width: accountSelector.width
control.contentItem: WalletAccountListItem {
readonly property var account: accountSelector.currentAccount
width: accountSelector.width
height: accountSelector.height
name: !!account ? account.name : ""
address: !!account ? account.address : ""
emoji: !!account ? account.emoji : ""
walletColor: !!account ? account.color : ""
leftPadding: 0
rightPadding: 0
statusListItemTitle.customColor: Theme.palette.directColor1
enabled: false
}
}
StatusBaseText {
text: qsTr("On")
color: Theme.palette.directColor5
font.weight: Font.Medium
}
StatusComboBox {
id: networkSelector
objectName: "networkSelector"
Layout.fillWidth: true
Layout.preferredHeight: 64
readonly property ModelEntry singleSelectionItem: ModelEntry {
sourceModel: root.store.flatNetworksModel
key: "chainId"
value: root.selectedNetworkChainId ?? -1
}
model: root.store.flatNetworksModel
control.background: SQP.StatusComboboxBackground {
active: networkSelector.control.down || networkSelector.control.hovered
}
component NetworkDelegate: StatusListItem {
required property var network
width: parent.width
title: network.chainName
asset.height: 36
asset.width: 36
asset.isImage: true
asset.name: Theme.svg(network.iconUrl)
subTitle: qsTr("Only")
}
control.contentItem: NetworkDelegate {
required property var model
network: networkSelector.singleSelectionItem.item
leftPadding: 0
rightPadding: 0
statusListItemTitle.customColor: Theme.palette.directColor1
bgColor: "transparent"
enabled: false
}
popup.verticalPadding: 0
delegate: NetworkDelegate {
required property var model
network: model
onClicked: {
root.selectedNetworkChainId = model.chainId
networkSelector.popup.close()
}
}
}
}
}

View File

@ -1 +1,2 @@
PinnedMessagesPopup 1.0 PinnedMessagesPopup.qml PinnedMessagesPopup 1.0 PinnedMessagesPopup.qml
RequestPaymentModal 1.0 RequestPaymentModal.qml

View File

@ -11,6 +11,7 @@ import StatusQ.Validators 0.1
import utils 1.0 import utils 1.0
import shared.controls 1.0 import shared.controls 1.0
import shared.panels 1.0
Control { Control {
id: root id: root
@ -73,6 +74,9 @@ Control {
property var formatBalance: balance => property var formatBalance: balance =>
`${balance.toLocaleString(Qt.locale())} CRYPTO` `${balance.toLocaleString(Qt.locale())} CRYPTO`
/* Shows separator between top and bottom text */
property bool showSeparator: false
/* Allows to set value to be displayed. The value is expected to be a not /* Allows to set value to be displayed. The value is expected to be a not
localized string like "1", "1.1" or "0.000000023400234222". Provided localized string like "1", "1.1" or "0.000000023400234222". Provided
value will be formatted and displayed. Depending on the fiatMode flag value will be formatted and displayed. Depending on the fiatMode flag
@ -217,6 +221,12 @@ Control {
} }
} }
Separator {
Layout.fillWidth: true
Layout.preferredHeight: 1
visible: root.showSeparator
}
StatusBaseText { StatusBaseText {
id: bottomItem id: bottomItem

View File

@ -15,11 +15,13 @@ import mainui 1.0
//TODO remove this dependency //TODO remove this dependency
import AppLayouts.Chat.panels 1.0 import AppLayouts.Chat.panels 1.0
import AppLayouts.Chat.popups 1.0
import AppLayouts.Chat.stores 1.0 as ChatStores import AppLayouts.Chat.stores 1.0 as ChatStores
import StatusQ 0.1 import StatusQ 0.1
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Popups 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils import StatusQ.Core.Utils 0.1 as StatusQUtils
import StatusQ.Components 0.1 import StatusQ.Components 0.1
import StatusQ.Controls 0.1 as StatusQ import StatusQ.Controls 0.1 as StatusQ
@ -40,6 +42,7 @@ Rectangle {
property var usersModel property var usersModel
property SharedStores.RootStore sharedStore property SharedStores.RootStore sharedStore
property SharedStores.RequestPaymentStore requestPaymentStore
property var emojiPopup: null property var emojiPopup: null
property var stickersPopup: null property var stickersPopup: null
@ -154,6 +157,9 @@ Rectangle {
property bool emojiPopupOpened: false property bool emojiPopupOpened: false
property bool stickersPopupOpened: false property bool stickersPopupOpened: false
property var imageDialog: null
property var requestPaymentPopup: null
// common popups are emoji, jif and stickers // common popups are emoji, jif and stickers
// Put controlWidth as argument with default value for binding // Put controlWidth as argument with default value for binding
function getCommonPopupRelativePosition(popup, popupParent, controlWidth = control.width) { function getCommonPopupRelativePosition(popup, popupParent, controlWidth = control.width) {
@ -355,7 +361,7 @@ Rectangle {
property var mentionsPos: [] property var mentionsPos: []
function isUploadFilePressed(event) { function isUploadFilePressed(event) {
return (event.key === Qt.Key_U) && (event.modifiers & Qt.ControlModifier) && imageBtn.visible && !imageBtn.highlighted return (event.key === Qt.Key_U) && (event.modifiers & Qt.ControlModifier) && !d.imageDialog
} }
function checkTextInsert() { function checkTextInsert() {
@ -535,7 +541,7 @@ Rectangle {
// U // U
if (isUploadFilePressed(event)) { if (isUploadFilePressed(event)) {
imageBtn.clicked(null) openImageDialog()
event.accepted = true event.accepted = true
} }
@ -949,6 +955,16 @@ Rectangle {
messageInputField.forceActiveFocus(); messageInputField.forceActiveFocus();
} }
function openImageDialog() {
d.imageDialog = imageDialogComponent.createObject(control)
d.imageDialog.open()
}
function openPaymentRequestPopup() {
d.requestPaymentPopup = requestPaymentPopupComponent.createObject(control)
d.requestPaymentPopup.open()
}
DropAreaPanel { DropAreaPanel {
enabled: control.visible && control.enabled enabled: control.visible && control.enabled
parent: Overlay.overlay parent: Overlay.overlay
@ -985,16 +1001,48 @@ Rectangle {
qsTr("Image files (%1)").arg(UrlUtils.validImageNameFilters) qsTr("Image files (%1)").arg(UrlUtils.validImageNameFilters)
] ]
onAccepted: { onAccepted: {
imageBtn.highlighted = false
validateImagesAndShowImageArea(fileUrls) validateImagesAndShowImageArea(fileUrls)
messageInputField.forceActiveFocus() messageInputField.forceActiveFocus()
destroy()
} }
onRejected: { onRejected: destroy()
imageBtn.highlighted = false Component.onDestruction: d.imageDialog = null
}
}
Component {
id: requestPaymentPopupComponent
RequestPaymentModal {
store: control.requestPaymentStore
onAccepted: {
control.requestPaymentStore.addPaymentRequest(selectedTokenKey, amount, selectedAccountAddress, selectedNetworkChainId)
} }
} }
} }
Component {
id: chatCommandMenuComponent
StatusMenu {
StatusAction {
text: qsTr("Add image")
icon.name: "image"
onTriggered: control.openImageDialog()
}
StatusAction {
text: qsTr("Add payment request")
icon.name: "wallet"
// TODO_ES disable for testnet (only production)
// TODO_ES error message when disabled on testnet (only production)
onTriggered: control.openPaymentRequestPopup()
}
closeHandler: () => commandBtn.highlighted = false
}
}
StatusEmojiSuggestionPopup { StatusEmojiSuggestionPopup {
id: emojiSuggestions id: emojiSuggestions
messageInput: messageInput messageInput: messageInput
@ -1068,18 +1116,19 @@ Rectangle {
spacing: 4 spacing: 4
StatusQ.StatusFlatRoundButton { StatusQ.StatusFlatRoundButton {
id: imageBtn id: commandBtn
Layout.preferredWidth: 32 Layout.preferredWidth: 32
Layout.preferredHeight: 32 Layout.preferredHeight: 32
Layout.alignment: Qt.AlignBottom Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: 4 Layout.bottomMargin: 4
icon.name: "image" icon.name: "chat-commands"
type: StatusQ.StatusFlatRoundButton.Type.Tertiary type: StatusQ.StatusFlatRoundButton.Type.Tertiary
visible: !isEdit visible: !isEdit
onClicked: { onClicked: {
highlighted = true highlighted = true
const popup = imageDialogComponent.createObject(control) let menu = chatCommandMenuComponent.createObject(commandBtn)
popup.open() menu.y = -menu.height // Show above button
menu.open()
} }
} }
@ -1224,6 +1273,7 @@ Rectangle {
topPadding: 12 topPadding: 12
imagePreviewArray: control.fileUrlsAndSources imagePreviewArray: control.fileUrlsAndSources
linkPreviewModel: control.linkPreviewModel linkPreviewModel: control.linkPreviewModel
requestPaymentModel: control.requestPaymentStore.requestPaymentModel
showLinkPreviewSettings: control.askToEnableLinkPreview showLinkPreviewSettings: control.askToEnableLinkPreview
onImageRemoved: (index) => { onImageRemoved: (index) => {
//Just do a copy and replace the whole thing because it's a plain JS array and thre's no signal when a single item is removed //Just do a copy and replace the whole thing because it's a plain JS array and thre's no signal when a single item is removed
@ -1242,6 +1292,7 @@ Rectangle {
onDisableLinkPreview: () => control.disableLinkPreview() onDisableLinkPreview: () => control.disableLinkPreview()
onDismissLinkPreviewSettings: () => control.dismissLinkPreviewSettings() onDismissLinkPreviewSettings: () => control.dismissLinkPreviewSettings()
onDismissLinkPreview: (index) => control.dismissLinkPreview(index) onDismissLinkPreview: (index) => control.dismissLinkPreview(index)
onPaymentRequestRemoved: (index) => control.requestPaymentStore.removePaymentRequest(index)
} }
RowLayout { RowLayout {

View File

@ -0,0 +1,8 @@
import QtQuick 2.15
QtObject {
required property CurrenciesStore currencyStore
required property var flatNetworksModel
required property var processedAssetsModel
required property var accountsModel
}

View File

@ -6,5 +6,6 @@ GifStore 1.0 GifStore.qml
MetricsStore 1.0 MetricsStore.qml MetricsStore 1.0 MetricsStore.qml
NetworkConnectionStore 1.0 NetworkConnectionStore.qml NetworkConnectionStore 1.0 NetworkConnectionStore.qml
PermissionsStore 1.0 PermissionsStore.qml PermissionsStore 1.0 PermissionsStore.qml
RequestPaymentStore 1.0 RequestPaymentStore.qml
RootStore 1.0 RootStore.qml RootStore 1.0 RootStore.qml
UtilsStore 1.0 UtilsStore.qml UtilsStore 1.0 UtilsStore.qml