From da224aa9f3c3f6822b6fbf10348983cdbe8ccd98 Mon Sep 17 00:00:00 2001 From: sirmorrison Date: Thu, 12 Oct 2023 10:39:51 +0100 Subject: [PATCH] optimize send page code to accomodate send modal --- ui/page/components/receive_modal.go | 2 +- ui/page/send/layout.go | 242 +++++------ ui/page/send/manual_coin_selection.go | 2 +- ui/page/send/page.go | 506 +--------------------- ui/page/send/send_amount.go | 51 ++- ui/page/send/send_destination.go | 16 +- ui/page/send/send_modal.go | 69 +-- ui/page/send/shared_widgets.go | 580 ++++++++++++++++++++++++++ 8 files changed, 752 insertions(+), 716 deletions(-) create mode 100644 ui/page/send/shared_widgets.go diff --git a/ui/page/components/receive_modal.go b/ui/page/components/receive_modal.go index a65f3c356..0fb9f63bd 100644 --- a/ui/page/components/receive_modal.go +++ b/ui/page/components/receive_modal.go @@ -50,7 +50,7 @@ type ReceiveModal struct { func NewReceiveModal(l *load.Load) *ReceiveModal { rm := &ReceiveModal{ Load: l, - Modal: l.Theme.ModalFloatTitle(values.String(values.StrSettings)), + Modal: l.Theme.ModalFloatTitle(values.String(values.StrReceive)), copyRedirect: l.Theme.NewClickable(false), info: l.Theme.IconButton(cryptomaterial.MustIcon(widget.NewIcon(icons.ActionInfo))), more: l.Theme.IconButton(l.Theme.Icons.NavigationMore), diff --git a/ui/page/send/layout.go b/ui/page/send/layout.go index a8a191759..6b606e8ea 100644 --- a/ui/page/send/layout.go +++ b/ui/page/send/layout.go @@ -4,7 +4,6 @@ import ( "fmt" "gioui.org/layout" - "gioui.org/widget" "github.com/crypto-power/cryptopower/app" libUtil "github.com/crypto-power/cryptopower/libwallet/utils" @@ -18,104 +17,56 @@ type ( D = layout.Dimensions ) -func (pg *Page) initLayoutWidgets() { - pg.pageContainer = &widget.List{ - List: layout.List{ - Axis: layout.Vertical, - Alignment: layout.Middle, - }, - } - - buttonInset := layout.Inset{ - Top: values.MarginPadding4, - Right: values.MarginPadding8, - Bottom: values.MarginPadding4, - Left: values.MarginPadding8, - } - - pg.nextButton = pg.Theme.Button(values.String(values.StrNext)) - pg.nextButton.TextSize = values.TextSize18 - pg.nextButton.Inset = layout.Inset{Top: values.MarginPadding15, Bottom: values.MarginPadding15} - pg.nextButton.SetEnabled(false) - - _, pg.infoButton = components.SubpageHeaderButtons(pg.Load) - - pg.retryExchange = pg.Theme.Button(values.String(values.StrRetry)) - pg.retryExchange.Background = pg.Theme.Color.Gray1 - pg.retryExchange.Color = pg.Theme.Color.Surface - pg.retryExchange.TextSize = values.TextSize12 - pg.retryExchange.Inset = buttonInset - - pg.txLabelInputEditor = pg.Theme.Editor(new(widget.Editor), values.String(values.StrNote)) - pg.txLabelInputEditor.Editor.SingleLine = false - pg.txLabelInputEditor.Editor.SetText("") - // Set the maximum characters the editor can accept. - pg.txLabelInputEditor.Editor.MaxLen = MaxTxLabelSize - - pg.toCoinSelection = pg.Theme.NewClickable(false) -} - -func (pg *Page) topNav(gtx C) D { +func (wi *widgetInitializer) topNav(gtx C) D { return layout.Flex{}.Layout(gtx, layout.Rigid(func(gtx C) D { return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Rigid(pg.Theme.H6(values.String(values.StrSend)+" "+string(pg.WL.SelectedWallet.Wallet.GetAssetType())).Layout), + layout.Rigid(wi.Theme.H6(values.String(values.StrSend)+" "+string(wi.selectedWallet.Asset.GetAssetType())).Layout), ) }), layout.Flexed(1, func(gtx C) D { return layout.E.Layout(gtx, func(gtx C) D { return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Rigid(pg.infoButton.Layout), + layout.Rigid(wi.infoButton.Layout), ) }) }), ) } -// Layout draws the page UI components into the provided layout context -// to be eventually drawn on screen. -// Part of the load.Page interface. -func (pg *Page) Layout(gtx C) D { - if pg.Load.GetCurrentAppWidth() <= gtx.Dp(values.StartMobileView) { - return pg.layoutMobile(gtx) - } - return pg.layoutDesktop(pg.ParentWindow(), nil, gtx) -} - -func (pg *Page) layoutDesktop(window app.WindowNavigator, walletSelector layout.Widget, gtx C) D { - // set the parent window this is needed for the send modal - // on the home page. - if pg.parentWindow == nil { - pg.parentWindow = window +func (wi *widgetInitializer) layoutDesktop(gtx C, window app.WindowNavigator) D { + if wi.parentWindow == nil { + wi.parentWindow = window } - pageContent := []func(gtx C) D{ func(gtx C) D { - return pg.pageSections(gtx, values.String(values.StrFrom), false, func(gtx C) D { + return wi.pageSections(gtx, values.String(values.StrFrom), false, func(gtx C) D { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { - if walletSelector != nil { + if wi.isModalLayout { return layout.Inset{ Bottom: values.MarginPadding16, - }.Layout(gtx, walletSelector) + }.Layout(gtx, func(gtx C) D { + return wi.sourceWalletSelector.Layout(wi.parentWindow, gtx) + }) } return D{} }), layout.Rigid(func(gtx C) D { - return pg.sourceAccountSelector.Layout(pg.parentWindow, gtx) + return wi.sourceAccountSelector.Layout(wi.parentWindow, gtx) }), ) }) }, - pg.toSection, - pg.coinSelectionSection, - pg.txLabelSection, + wi.toSection, + wi.coinSelectionSection, + wi.txLabelSection, } // Display the transaction fee rate selection only for btc and ltc wallets. - switch pg.selectedWallet.GetAssetType() { + switch wi.selectedWallet.GetAssetType() { case libUtil.BTCWalletAsset, libUtil.LTCWalletAsset: - pageContent = append(pageContent, pg.feeRateSelector.Layout) + pageContent = append(pageContent, wi.feeRateSelector.Layout) } // Add the bottom spacing section as the last. @@ -133,10 +84,10 @@ func (pg *Page) layoutDesktop(window app.WindowNavigator, walletSelector layout. return components.UniformPadding(gtx, func(gtx C) D { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { - return layout.Inset{Bottom: values.MarginPadding16}.Layout(gtx, pg.topNav) + return layout.Inset{Bottom: values.MarginPadding16}.Layout(gtx, wi.topNav) }), layout.Rigid(func(gtx C) D { - return pg.Theme.List(pg.pageContainer).Layout(gtx, len(pageContent), func(gtx C, i int) D { + return wi.Theme.List(wi.pageContainer).Layout(gtx, len(pageContent), func(gtx C, i int) D { return layout.Inset{Right: values.MarginPadding2}.Layout(gtx, func(gtx C) D { return layout.Inset{Bottom: values.MarginPadding4, Top: values.MarginPadding4}.Layout(gtx, pageContent[i]) }) @@ -150,7 +101,7 @@ func (pg *Page) layoutDesktop(window app.WindowNavigator, walletSelector layout. layout.Stacked(func(gtx C) D { gtx.Constraints.Min.Y = gtx.Constraints.Max.Y return layout.S.Layout(gtx, func(gtx C) D { - return layout.Inset{Left: values.MarginPadding1}.Layout(gtx, pg.balanceSection) + return layout.Inset{Left: values.MarginPadding1}.Layout(gtx, wi.balanceSection) }) }), ) @@ -158,19 +109,15 @@ func (pg *Page) layoutDesktop(window app.WindowNavigator, walletSelector layout. return dims } -func (pg *Page) layoutMobile(gtx C) D { +func (wi *widgetInitializer) layoutMobile(gtx C) D { pageContent := []func(gtx C) D{ func(gtx C) D { - return pg.pageSections(gtx, values.String(values.StrFrom), false, func(gtx C) D { - return pg.sourceAccountSelector.Layout(pg.parentWindow, gtx) + return wi.pageSections(gtx, values.String(values.StrFrom), false, func(gtx C) D { + return wi.sourceAccountSelector.Layout(wi.parentWindow, gtx) }) }, - func(gtx C) D { - return pg.toSection(gtx) - }, - func(gtx C) D { - return pg.coinSelectionSection(gtx) - }, + wi.toSection, + wi.coinSelectionSection, } dims := layout.Stack{Alignment: layout.S}.Layout(gtx, @@ -180,14 +127,21 @@ func (pg *Page) layoutMobile(gtx C) D { return components.UniformMobile(gtx, false, true, func(gtx C) D { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { - return layout.Inset{Bottom: values.MarginPadding16, Right: values.MarginPadding10}.Layout(gtx, func(gtx C) D { - return pg.topNav(gtx) - }) + return layout.Inset{ + Bottom: values.MarginPadding16, + Right: values.MarginPadding10, + }.Layout(gtx, wi.topNav) }), layout.Rigid(func(gtx C) D { - return pg.Theme.List(pg.pageContainer).Layout(gtx, len(pageContent), func(gtx C, i int) D { - return layout.Inset{Bottom: values.MarginPadding16, Right: values.MarginPadding2}.Layout(gtx, func(gtx C) D { - return layout.Inset{Bottom: values.MarginPadding4, Top: values.MarginPadding4}.Layout(gtx, pageContent[i]) + return wi.Theme.List(wi.pageContainer).Layout(gtx, len(pageContent), func(gtx C, i int) D { + return layout.Inset{ + Bottom: values.MarginPadding16, + Right: values.MarginPadding2, + }.Layout(gtx, func(gtx C) D { + return layout.Inset{ + Bottom: values.MarginPadding4, + Top: values.MarginPadding4, + }.Layout(gtx, pageContent[i]) }) }) }), @@ -199,9 +153,7 @@ func (pg *Page) layoutMobile(gtx C) D { layout.Stacked(func(gtx C) D { gtx.Constraints.Min.Y = gtx.Constraints.Max.Y return layout.S.Layout(gtx, func(gtx C) D { - return layout.Inset{Left: values.MarginPadding1}.Layout(gtx, func(gtx C) D { - return pg.balanceSection(gtx) - }) + return layout.Inset{Left: values.MarginPadding1}.Layout(gtx, wi.balanceSection) }) }), ) @@ -209,8 +161,8 @@ func (pg *Page) layoutMobile(gtx C) D { return dims } -func (pg *Page) pageSections(gtx C, title string, showAccountSwitch bool, body layout.Widget) D { - return pg.Theme.Card().Layout(gtx, func(gtx C) D { +func (wi *widgetInitializer) pageSections(gtx C, title string, showAccountSwitch bool, body layout.Widget) D { + return wi.Theme.Card().Layout(gtx, func(gtx C) D { return layout.UniformInset(values.MarginPadding16).Layout(gtx, func(gtx C) D { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { @@ -219,8 +171,8 @@ func (pg *Page) pageSections(gtx C, title string, showAccountSwitch bool, body l inset := layout.Inset{ Bottom: values.MarginPadding16, } - titleTxt := pg.Theme.Body1(title) - titleTxt.Color = pg.Theme.Color.Text + titleTxt := wi.Theme.Body1(title) + titleTxt.Color = wi.Theme.Color.Text return inset.Layout(gtx, titleTxt.Layout) }), layout.Flexed(1, func(gtx C) D { @@ -229,8 +181,8 @@ func (pg *Page) pageSections(gtx C, title string, showAccountSwitch bool, body l inset := layout.Inset{ Top: values.MarginPaddingMinus5, } - pg.sendDestination.accountSwitch.SetSelectedIndex(pg.sendDestination.selectedIndex) - return inset.Layout(gtx, pg.sendDestination.accountSwitch.Layout) + wi.sendDestination.accountSwitch.SetSelectedIndex(wi.sendDestination.selectedIndex) + return inset.Layout(gtx, wi.sendDestination.accountSwitch.Layout) }) } return D{} @@ -243,14 +195,14 @@ func (pg *Page) pageSections(gtx C, title string, showAccountSwitch bool, body l }) } -func (pg *Page) toSection(gtx C) D { - return pg.pageSections(gtx, values.String(values.StrTo), true, func(gtx C) D { +func (wi *widgetInitializer) toSection(gtx C) D { + return wi.pageSections(gtx, values.String(values.StrTo), true, func(gtx C) D { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { return layout.Inset{ Bottom: values.MarginPadding16, }.Layout(gtx, func(gtx C) D { - if !pg.sendDestination.sendToAddress { + if !wi.sendDestination.sendToAddress { return cryptomaterial.LinearLayout{ Width: cryptomaterial.MatchParent, Height: cryptomaterial.WrapContent, @@ -261,41 +213,37 @@ func (pg *Page) toSection(gtx C) D { return layout.Inset{ Bottom: values.MarginPadding16, }.Layout(gtx, func(gtx C) D { - return pg.sendDestination.destinationWalletSelector.Layout(pg.parentWindow, gtx) + return wi.sendDestination.destinationWalletSelector.Layout(wi.parentWindow, gtx) }) }), layout.Rigid(func(gtx C) D { - return pg.sendDestination.destinationAccountSelector.Layout(pg.parentWindow, gtx) + return wi.sendDestination.destinationAccountSelector.Layout(wi.parentWindow, gtx) }), ) } - return pg.sendDestination.destinationAddressEditor.Layout(gtx) + return wi.sendDestination.destinationAddressEditor.Layout(gtx) }) }), layout.Rigid(func(gtx C) D { - if pg.exchangeRate != -1 && pg.usdExchangeSet { + if wi.exchangeRate != -1 && wi.usdExchangeSet { return layout.Flex{ Axis: layout.Horizontal, Alignment: layout.Middle, }.Layout(gtx, - layout.Flexed(0.45, func(gtx C) D { - return pg.amount.amountEditor.Layout(gtx) - }), + layout.Flexed(0.45, wi.amount.amountEditor.Layout), layout.Flexed(0.1, func(gtx C) D { return layout.Center.Layout(gtx, func(gtx C) D { - icon := pg.Theme.Icons.CurrencySwapIcon + icon := wi.Theme.Icons.CurrencySwapIcon return icon.Layout12dp(gtx) }) }), - layout.Flexed(0.45, func(gtx C) D { - return pg.amount.usdAmountEditor.Layout(gtx) - }), + layout.Flexed(0.45, wi.amount.usdAmountEditor.Layout), ) } - return pg.amount.amountEditor.Layout(gtx) + return wi.amount.amountEditor.Layout(gtx) }), layout.Rigid(func(gtx C) D { - if pg.exchangeRateMessage == "" { + if wi.exchangeRateMessage == "" { return D{} } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, @@ -303,25 +251,25 @@ func (pg *Page) toSection(gtx C) D { return layout.Inset{Top: values.MarginPadding16, Bottom: values.MarginPadding16}.Layout(gtx, func(gtx C) D { gtx.Constraints.Min.X = gtx.Constraints.Max.X gtx.Constraints.Min.Y = gtx.Dp(values.MarginPadding1) - return cryptomaterial.Fill(gtx, pg.Theme.Color.Gray1) + return cryptomaterial.Fill(gtx, wi.Theme.Color.Gray1) }) }), layout.Rigid(func(gtx C) D { return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, layout.Rigid(func(gtx C) D { - label := pg.Theme.Body2(pg.exchangeRateMessage) - label.Color = pg.Theme.Color.Danger - if pg.isFetchingExchangeRate { - label.Color = pg.Theme.Color.Primary + label := wi.Theme.Body2(wi.exchangeRateMessage) + label.Color = wi.Theme.Color.Danger + if wi.isFetchingExchangeRate { + label.Color = wi.Theme.Color.Primary } return label.Layout(gtx) }), layout.Rigid(func(gtx C) D { - if pg.isFetchingExchangeRate { + if wi.isFetchingExchangeRate { return D{} } gtx.Constraints.Min.X = gtx.Constraints.Max.X - return layout.E.Layout(gtx, pg.retryExchange.Layout) + return layout.E.Layout(gtx, wi.retryExchange.Layout) }), ) }), @@ -331,17 +279,17 @@ func (pg *Page) toSection(gtx C) D { }) } -func (pg *Page) coinSelectionSection(gtx C) D { +func (wi *widgetInitializer) coinSelectionSection(gtx C) D { selectedOption := automaticCoinSelection - sourceAcc := pg.sourceAccountSelector.SelectedAccount() - if len(pg.selectedUTXOs.selectedUTXOs) > 0 && pg.selectedUTXOs.sourceAccount == sourceAcc { + sourceAcc := wi.sourceAccountSelector.SelectedAccount() + if len(wi.selectedUTXOs.selectedUTXOs) > 0 && wi.selectedUTXOs.sourceAccount == sourceAcc { selectedOption = manualCoinSelection } - return pg.Theme.Card().Layout(gtx, func(gtx C) D { + return wi.Theme.Card().Layout(gtx, func(gtx C) D { inset := layout.UniformInset(values.MarginPadding15) return inset.Layout(gtx, func(gtx C) D { - textLabel := pg.Theme.Label(values.TextSize16, values.String(values.StrCoinSelection)) + textLabel := wi.Theme.Label(values.TextSize16, values.String(values.StrCoinSelection)) return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, layout.Rigid(textLabel.Layout), layout.Flexed(1, func(gtx C) D { @@ -351,10 +299,10 @@ func (pg *Page) coinSelectionSection(gtx C) D { Height: cryptomaterial.WrapContent, Orientation: layout.Horizontal, Alignment: layout.Middle, - Clickable: pg.toCoinSelection, + Clickable: wi.toCoinSelection, }.Layout(gtx, - layout.Rigid(pg.Theme.Label(values.TextSize16, selectedOption).Layout), - layout.Rigid(pg.Theme.Icons.ChevronRight.Layout24dp), + layout.Rigid(wi.Theme.Label(values.TextSize16, selectedOption).Layout), + layout.Rigid(wi.Theme.Icons.ChevronRight.Layout24dp), ) }) }), @@ -363,14 +311,14 @@ func (pg *Page) coinSelectionSection(gtx C) D { }) } -func (pg *Page) txLabelSection(gtx C) D { - return pg.Theme.Card().Layout(gtx, func(gtx C) D { +func (wi *widgetInitializer) txLabelSection(gtx C) D { + return wi.Theme.Card().Layout(gtx, func(gtx C) D { topContainer := layout.UniformInset(values.MarginPadding15) return topContainer.Layout(gtx, func(gtx C) D { - textLabel := pg.Theme.Label(values.TextSize16, values.String(values.StrDescriptionNote)) - count := len(pg.txLabelInputEditor.Editor.Text()) - txt := fmt.Sprintf("(%d/%d)", count, pg.txLabelInputEditor.Editor.MaxLen) - wordsCount := pg.Theme.Label(values.TextSize14, txt) + textLabel := wi.Theme.Label(values.TextSize16, values.String(values.StrDescriptionNote)) + count := len(wi.txLabelInputEditor.Editor.Text()) + txt := fmt.Sprintf("(%d/%d)", count, wi.txLabelInputEditor.Editor.MaxLen) + wordsCount := wi.Theme.Label(values.TextSize14, txt) return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, @@ -389,7 +337,7 @@ func (pg *Page) txLabelSection(gtx C) D { return layout.Inset{ Top: values.MarginPadding10, }.Layout(gtx, func(gtx C) D { - return pg.txLabelInputEditor.Layout(gtx) + return wi.txLabelInputEditor.Layout(gtx) }) }), ) @@ -397,8 +345,8 @@ func (pg *Page) txLabelSection(gtx C) D { }) } -func (pg *Page) balanceSection(gtx C) D { - c := pg.Theme.Card() +func (wi *widgetInitializer) balanceSection(gtx C) D { + c := wi.Theme.Card() c.Radius = cryptomaterial.Radius(0) return c.Layout(gtx, func(gtx C) D { inset := layout.Inset{ @@ -416,43 +364,43 @@ func (pg *Page) balanceSection(gtx C) D { return inset.Layout(gtx, func(gtx C) D { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { - totalCostText := pg.totalCost - if pg.exchangeRate != -1 && pg.usdExchangeSet { - totalCostText = fmt.Sprintf("%s (%s)", pg.totalCost, pg.totalCostUSD) + totalCostText := wi.totalCost + if wi.exchangeRate != -1 && wi.usdExchangeSet { + totalCostText = fmt.Sprintf("%s (%s)", wi.totalCost, wi.totalCostUSD) } - return pg.contentRow(gtx, values.String(values.StrTotalCost), totalCostText) + return wi.contentRow(gtx, values.String(values.StrTotalCost), totalCostText) }), layout.Rigid(func(gtx C) D { - balanceAfterSendText := pg.balanceAfterSend - if pg.exchangeRate != -1 && pg.usdExchangeSet { - balanceAfterSendText = fmt.Sprintf("%s (%s)", pg.balanceAfterSend, pg.balanceAfterSendUSD) + balanceAfterSendText := wi.balanceAfterSend + if wi.exchangeRate != -1 && wi.usdExchangeSet { + balanceAfterSendText = fmt.Sprintf("%s (%s)", wi.balanceAfterSend, wi.balanceAfterSendUSD) } - return pg.contentRow(gtx, values.String(values.StrBalanceAfter), balanceAfterSendText) + return wi.contentRow(gtx, values.String(values.StrBalanceAfter), balanceAfterSendText) }), ) }) }), layout.Flexed(0.3, func(gtx C) D { - return pg.nextButton.Layout(gtx) + return wi.nextButton.Layout(gtx) }), ) }) }) } -func (pg *Page) contentRow(gtx C, leftValue, rightValue string) D { +func (wi *widgetInitializer) contentRow(gtx C, leftValue, rightValue string) D { return layout.Flex{}.Layout(gtx, layout.Rigid(func(gtx C) D { - txt := pg.Theme.Body2(leftValue) - txt.Color = pg.Theme.Color.GrayText2 + txt := wi.Theme.Body2(leftValue) + txt.Color = wi.Theme.Color.GrayText2 return txt.Layout(gtx) }), layout.Flexed(1, func(gtx C) D { return layout.E.Layout(gtx, func(gtx C) D { return layout.Flex{}.Layout(gtx, layout.Rigid(func(gtx C) D { - rightText := pg.Theme.Body1(rightValue) - rightText.Color = pg.Theme.Color.Text + rightText := wi.Theme.Body1(rightValue) + rightText.Color = wi.Theme.Color.Text return rightText.Layout(gtx) }), ) diff --git a/ui/page/send/manual_coin_selection.go b/ui/page/send/manual_coin_selection.go index 98f353e4c..315b35a1f 100644 --- a/ui/page/send/manual_coin_selection.go +++ b/ui/page/send/manual_coin_selection.go @@ -261,7 +261,7 @@ func (pg *ManualCoinSelectionPage) fetchAccountsInfo() error { // Part of the load.Page interface. func (pg *ManualCoinSelectionPage) HandleUserInteractions() { if pg.actionButton.Clicked() { - pg.sendPage.UpdateSelectedUTXOs(pg.selectedUTXOrows) + pg.sendPage.updateSelectedUTXOs(pg.selectedUTXOrows) pg.ParentNavigator().CloseCurrentPage() } diff --git a/ui/page/send/page.go b/ui/page/send/page.go index cfa99a72c..02657f8f8 100644 --- a/ui/page/send/page.go +++ b/ui/page/send/page.go @@ -2,35 +2,17 @@ package send import ( "context" - "fmt" - "strings" "gioui.org/io/key" - "gioui.org/layout" - "gioui.org/widget" "github.com/crypto-power/cryptopower/app" - sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" - libUtil "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/crypto-power/cryptopower/ui/cryptomaterial" "github.com/crypto-power/cryptopower/ui/load" - "github.com/crypto-power/cryptopower/ui/modal" - "github.com/crypto-power/cryptopower/ui/page/components" - "github.com/crypto-power/cryptopower/ui/utils" "github.com/crypto-power/cryptopower/ui/values" ) const ( SendPageID = "Send" - - // MaxTxLabelSize defines the maximum number of characters to be allowed on - // txLabelInputEditor component. - MaxTxLabelSize = 100 -) - -var ( - automaticCoinSelection = values.String(values.StrAutomatic) - manualCoinSelection = values.String(values.StrManual) ) type Page struct { @@ -41,224 +23,43 @@ type Page struct { // and the root WindowNavigator. *app.GenericPageModal - parentWindow app.WindowNavigator - ctx context.Context // page context ctxCancel context.CancelFunc - pageContainer *widget.List - - sourceAccountSelector *components.WalletAndAccountSelector - sendDestination *destination - amount *sendAmount - - infoButton cryptomaterial.IconButton - retryExchange cryptomaterial.Button - nextButton cryptomaterial.Button - - shadowBox *cryptomaterial.Shadow - backdrop *widget.Clickable - - isFetchingExchangeRate bool - - exchangeRate float64 - usdExchangeSet bool - exchangeRateMessage string - confirmTxModal *sendConfirmModal - currencyExchange string - - txLabelInputEditor cryptomaterial.Editor - - *authoredTxData - selectedWallet *load.WalletMapping - feeRateSelector *components.FeeRateSelector - - toCoinSelection *cryptomaterial.Clickable - - selectedUTXOs selectedUTXOsInfo -} - -type authoredTxData struct { - destinationAddress string - destinationAccount *sharedW.Account - sourceAccount *sharedW.Account - txFee string - txFeeUSD string - totalCost string - totalCostUSD string - balanceAfterSend string - balanceAfterSendUSD string - sendAmount string - sendAmountUSD string -} - -type selectedUTXOsInfo struct { - sourceAccount *sharedW.Account - selectedUTXOs []*sharedW.UnspentOutput - totalUTXOsAmount int64 + *widgetInitializer } func NewSendPage(l *load.Load) *Page { pg := &Page{ - Load: l, - GenericPageModal: app.NewGenericPageModal(SendPageID), - sendDestination: newSendDestination(l), - amount: newSendAmount(l), - - exchangeRate: -1, - - authoredTxData: &authoredTxData{}, - shadowBox: l.Theme.Shadow(), - backdrop: new(widget.Clickable), - } - pg.selectedWallet = &load.WalletMapping{ - Asset: l.WL.SelectedWallet.Wallet, - } - - callbackFunc := func() libUtil.AssetType { - return pg.selectedWallet.GetAssetType() + Load: l, + GenericPageModal: app.NewGenericPageModal(SendPageID), + widgetInitializer: newWidgetInitializer(l, false), } - pg.feeRateSelector = components.NewFeeRateSelector(l, callbackFunc).ShowSizeAndCost() - pg.feeRateSelector.TitleInset = layout.Inset{Bottom: values.MarginPadding10} - pg.feeRateSelector.ContainerInset = layout.Inset{Bottom: values.MarginPadding100} - pg.feeRateSelector.WrapperInset = layout.UniformInset(values.MarginPadding15) - - pg.initializeAccountSelectors() - - pg.sendDestination.addressChanged = func() { - pg.validateAndConstructTx() - } - - pg.amount.amountChanged = func() { - pg.validateAndConstructTxAmountOnly() - } - - pg.initLayoutWidgets() return pg } -// RestyleWidgets restyles select widgets to match the current theme. This is -// especially necessary when the dark mode setting is changed. -func (pg *Page) RestyleWidgets() { - pg.amount.styleWidgets() - pg.sendDestination.styleWidgets() -} - -func (pg *Page) initializeAccountSelectors() { - // Source account picker - pg.sourceAccountSelector = components.NewWalletAndAccountSelector(pg.Load). - Title(values.String(values.StrFrom)). - AccountSelected(func(selectedAccount *sharedW.Account) { - // this resets the selected destination account based on the - // selected source account. This is done to prevent sending to - // an account that is invalid either because the destination - // account is the same as the source account or because the - // destination account needs to change based on if the selected - // wallet has privacy enabled. - pg.sendDestination.destinationAccountSelector.SelectFirstValidAccount( - pg.sendDestination.destinationWalletSelector.SelectedWallet()) - pg.validateAndConstructTx() - }). - AccountValidator(func(account *sharedW.Account) bool { - accountIsValid := account.Number != load.MaxInt32 && !pg.selectedWallet.IsWatchingOnlyWallet() - - if pg.selectedWallet.ReadBoolConfigValueForKey(sharedW.AccountMixerConfigSet, false) && - !pg.selectedWallet.ReadBoolConfigValueForKey(sharedW.SpendUnmixedFundsKey, false) { - // Spending unmixed fund isn't permitted for the selected wallet - - // only mixed accounts can send to address/wallets for wallet with privacy setup - switch pg.sendDestination.accountSwitch.SelectedIndex() { - case sendToAddress: - accountIsValid = account.Number == pg.selectedWallet.MixedAccountNumber() - case SendToWallet: - destinationWalletID := pg.sendDestination.destinationWalletSelector.SelectedWallet().GetWalletID() - if destinationWalletID != pg.selectedWallet.GetWalletID() { - accountIsValid = account.Number == pg.selectedWallet.MixedAccountNumber() - } - } - } - return accountIsValid - }). - SetActionInfoText(values.String(values.StrTxConfModalInfoTxt)) - - // if a source account exists, don't overwrite it. - if pg.sourceAccountSelector.SelectedAccount() == nil { - pg.sourceAccountSelector.SelectFirstValidAccount(pg.selectedWallet) - } - - pg.sendDestination.destinationAccountSelector = pg.sendDestination.destinationAccountSelector.AccountValidator(func(account *sharedW.Account) bool { - accountIsValid := account.Number != load.MaxInt32 - // Filter mixed wallet - destinationWallet := pg.sendDestination.destinationAccountSelector.SelectedWallet() - isMixedAccount := destinationWallet.MixedAccountNumber() == account.Number - // Filter the sending account. - sourceWalletID := pg.sourceAccountSelector.SelectedAccount().WalletID - isSameAccount := sourceWalletID == account.WalletID && account.Number == pg.sourceAccountSelector.SelectedAccount().Number - if !accountIsValid || isSameAccount || isMixedAccount { - return false - } - return true - }) - - pg.sendDestination.destinationAccountSelector.AccountSelected(func(selectedAccount *sharedW.Account) { - pg.validateAndConstructTx() - }) - - pg.sendDestination.destinationWalletSelector.WalletSelected(func(selectedWallet *load.WalletMapping) { - pg.sendDestination.destinationAccountSelector.SelectFirstValidAccount(selectedWallet) - if pg.selectedWallet.Asset.GetAssetType() == libUtil.DCRWalletAsset { - pg.sourceAccountSelector.SelectFirstValidAccount(pg.selectedWallet) - } - }) -} - -func (pg *Page) UpdateSelectedUTXOs(utxos []*sharedW.UnspentOutput) { - pg.selectedUTXOs = selectedUTXOsInfo{ - selectedUTXOs: utxos, - sourceAccount: pg.sourceAccountSelector.SelectedAccount(), - } - if len(utxos) > 0 { - for _, elem := range utxos { - pg.selectedUTXOs.totalUTXOsAmount += elem.Amount.ToInt() - } - } -} - // OnNavigatedTo is called when the page is about to be displayed and // may be used to initialize page features that are only relevant when // the page is displayed. // Part of the load.Page interface. func (pg *Page) OnNavigatedTo() { - pg.RestyleWidgets() - pg.ctx, pg.ctxCancel = context.WithCancel(context.TODO()) - if !pg.WL.SelectedWallet.Wallet.IsSynced() { - // Events are disabled until the wallet is fully synced. - return - } pg.sourceAccountSelector.ListenForTxNotifications(pg.ctx, pg.ParentWindow()) - // destinationAccountSelector does not have a default value, - // so assign it an initial value here - pg.sendDestination.destinationAccountSelector.SelectFirstValidAccount(pg.sendDestination.destinationWalletSelector.SelectedWallet()) - pg.sendDestination.destinationAddressEditor.Editor.Focus() - pg.usdExchangeSet = false - if components.IsFetchExchangeRateAPIAllowed(pg.WL) { - pg.currencyExchange = pg.WL.AssetsManager.GetCurrencyConversionExchange() - pg.usdExchangeSet = true - go pg.fetchExchangeRate() - } else { - // If exchange rate is not supported, validate and construct the TX. - pg.validateAndConstructTx() - } + pg.onLoaded() +} - if pg.selectedWallet.GetAssetType() == libUtil.BTCWalletAsset && pg.isFeerateAPIApproved() { - // This API call may take sometime to return. Call this before and cache - // results. - go pg.selectedWallet.GetAPIFeeRate() +// Layout draws the page UI components into the provided layout context +// to be eventually drawn on screen. +// Part of the load.Page interface. +func (pg *Page) Layout(gtx C) D { + if pg.Load.GetCurrentAppWidth() <= gtx.Dp(values.StartMobileView) { + return pg.layoutMobile(gtx) } + + return pg.layoutDesktop(gtx, pg.ParentWindow()) } // OnDarkModeChanged is triggered whenever the dark mode setting is changed @@ -268,233 +69,13 @@ func (pg *Page) OnDarkModeChanged(_ bool) { pg.amount.styleWidgets() } -func (pg *Page) fetchExchangeRate() { - if pg.isFetchingExchangeRate { - return - } - pg.isFetchingExchangeRate = true - var market string - switch pg.WL.SelectedWallet.Wallet.GetAssetType() { - case libUtil.DCRWalletAsset: - market = values.DCRUSDTMarket - case libUtil.BTCWalletAsset: - market = values.BTCUSDTMarket - case libUtil.LTCWalletAsset: - market = values.LTCUSDTMarket - default: - log.Errorf("Unsupported asset type: %s", pg.WL.SelectedWallet.Wallet.GetAssetType()) - pg.isFetchingExchangeRate = false - return - } - - rate, err := pg.WL.AssetsManager.ExternalService.GetTicker(pg.currencyExchange, market) - if err != nil { - log.Error(err) - pg.isFetchingExchangeRate = false - return - } - - pg.exchangeRate = rate.LastTradePrice - pg.amount.setExchangeRate(pg.exchangeRate) - pg.validateAndConstructTx() // convert estimates to usd - - pg.isFetchingExchangeRate = false - pg.parentWindow.Reload() -} - -func (pg *Page) validateAndConstructTx() { - // delete all the previous errors set earlier. - pg.amountValidationError("") - pg.addressValidationError("") - - if pg.validate() { - pg.constructTx() - } else { - pg.clearEstimates() - pg.showBalaceAfterSend() - } -} - -func (pg *Page) validateAndConstructTxAmountOnly() { - defer pg.RefreshTheme(pg.parentWindow) - - if !pg.sendDestination.validate() && pg.amount.amountIsValid() { - pg.constructTx() - } else { - pg.validateAndConstructTx() - } -} - -func (pg *Page) validate() bool { - amountIsValid := pg.amount.amountIsValid() - addressIsValid := pg.sendDestination.validate() - - // No need for checking the err message since it is as result of amount and - // address validation. - // validForSending - return amountIsValid && addressIsValid -} - -func (pg *Page) constructTx() { - destinationAddress, err := pg.sendDestination.destinationAddress() - if err != nil { - pg.addressValidationError(err.Error()) - return - } - destinationAccount := pg.sendDestination.destinationAccount() - - amountAtom, SendMax, err := pg.amount.validAmount() - if err != nil { - pg.amountValidationError(err.Error()) - return - } - - sourceAccount := pg.sourceAccountSelector.SelectedAccount() - selectedUTXOs := make([]*sharedW.UnspentOutput, 0) - if sourceAccount == pg.selectedUTXOs.sourceAccount { - selectedUTXOs = pg.selectedUTXOs.selectedUTXOs - } - - err = pg.selectedWallet.NewUnsignedTx(sourceAccount.Number, selectedUTXOs) - if err != nil { - pg.amountValidationError(err.Error()) - return - } - - err = pg.selectedWallet.AddSendDestination(destinationAddress, amountAtom, SendMax) - if err != nil { - if strings.Contains(err.Error(), "amount") { - pg.amountValidationError(err.Error()) - return - } - pg.addressValidationError(err.Error()) - return - } - - feeAndSize, err := pg.selectedWallet.EstimateFeeAndSize() - if err != nil { - pg.amountValidationError(err.Error()) - return - } - - feeAtom := feeAndSize.Fee.UnitValue - spendableAmount := sourceAccount.Balance.Spendable.ToInt() - if len(selectedUTXOs) > 0 { - spendableAmount = pg.selectedUTXOs.totalUTXOsAmount - } - - if SendMax { - amountAtom = spendableAmount - feeAtom - } - - wal := pg.WL.SelectedWallet.Wallet - totalSendingAmount := wal.ToAmount(amountAtom + feeAtom) - balanceAfterSend := wal.ToAmount(spendableAmount - totalSendingAmount.ToInt()) - - // populate display data - pg.txFee = wal.ToAmount(feeAtom).String() - - pg.feeRateSelector.EstSignedSize = fmt.Sprintf("%d Bytes", feeAndSize.EstimatedSignedSize) - pg.feeRateSelector.TxFee = pg.txFee - pg.feeRateSelector.SetFeerate(feeAndSize.FeeRate) - pg.totalCost = totalSendingAmount.String() - pg.balanceAfterSend = balanceAfterSend.String() - pg.sendAmount = wal.ToAmount(amountAtom).String() - pg.destinationAddress = destinationAddress - pg.destinationAccount = destinationAccount - pg.sourceAccount = sourceAccount - - if SendMax { - // TODO: this workaround ignores the change events from the - // amount input to avoid construct tx cycle. - pg.amount.setAmount(amountAtom) - } - - if pg.exchangeRate != -1 && pg.usdExchangeSet { - pg.feeRateSelector.USDExchangeSet = true - pg.txFeeUSD = fmt.Sprintf("$%.4f", utils.CryptoToUSD(pg.exchangeRate, feeAndSize.Fee.CoinValue)) - pg.feeRateSelector.TxFeeUSD = pg.txFeeUSD - pg.totalCostUSD = utils.FormatUSDBalance(pg.Printer, utils.CryptoToUSD(pg.exchangeRate, totalSendingAmount.ToCoin())) - pg.balanceAfterSendUSD = utils.FormatUSDBalance(pg.Printer, utils.CryptoToUSD(pg.exchangeRate, balanceAfterSend.ToCoin())) - - usdAmount := utils.CryptoToUSD(pg.exchangeRate, wal.ToAmount(amountAtom).ToCoin()) - pg.sendAmountUSD = utils.FormatUSDBalance(pg.Printer, usdAmount) - } -} - -func (pg *Page) showBalaceAfterSend() { - if pg.sourceAccountSelector != nil { - sourceAccount := pg.sourceAccountSelector.SelectedAccount() - if sourceAccount.Balance == nil { - return - } - balanceAfterSend := sourceAccount.Balance.Spendable - pg.balanceAfterSend = balanceAfterSend.String() - pg.balanceAfterSendUSD = utils.FormatUSDBalance(pg.Printer, utils.CryptoToUSD(pg.exchangeRate, balanceAfterSend.ToCoin())) - } -} - -func (pg *Page) amountValidationError(err string) { - pg.amount.setError(err) - pg.clearEstimates() -} - -func (pg *Page) addressValidationError(err string) { - pg.sendDestination.setError(err) - pg.clearEstimates() -} - -func (pg *Page) clearEstimates() { - pg.txFee = " - " + string(pg.selectedWallet.GetAssetType()) - pg.feeRateSelector.TxFee = pg.txFee - pg.txFeeUSD = " - " - pg.feeRateSelector.TxFeeUSD = pg.txFeeUSD - pg.totalCost = " - " + string(pg.selectedWallet.GetAssetType()) - pg.totalCostUSD = " - " - pg.balanceAfterSend = " - " + string(pg.selectedWallet.GetAssetType()) - pg.balanceAfterSendUSD = " - " - pg.sendAmount = " - " - pg.sendAmountUSD = " - " - pg.feeRateSelector.SetFeerate(0) -} - -func (pg *Page) resetFields() { - pg.sendDestination.clearAddressInput() - pg.txLabelInputEditor.Editor.SetText("") - - pg.amount.resetFields() -} - // HandleUserInteractions is called just before Layout() to determine // if any user interaction recently occurred on the page and may be // used to update the page's UI components shortly before they are // displayed. // Part of the load.Page interface. func (pg *Page) HandleUserInteractions() { - if pg.feeRateSelector.FetchRates.Clicked() { - go pg.feeRateSelector.FetchFeeRate(pg.parentWindow, pg.selectedWallet) - } - - if pg.feeRateSelector.EditRates.Clicked() { - pg.feeRateSelector.OnEditRateClicked(pg.selectedWallet) - } - - pg.nextButton.SetEnabled(pg.validate()) - pg.sendDestination.handle() - pg.amount.handle() - - if pg.infoButton.Button.Clicked() { - textWithUnit := values.String(values.StrSend) + " " + string(pg.selectedWallet.GetAssetType()) - info := modal.NewCustomModal(pg.Load). - Title(textWithUnit). - Body(values.String(values.StrSendInfo)). - SetPositiveButtonText(values.String(values.StrGotIt)) - pg.parentWindow.ShowModal(info) - } - - if pg.retryExchange.Clicked() { - go pg.fetchExchangeRate() - } + pg.handleFunc() if pg.toCoinSelection.Clicked() { _, err := pg.sendDestination.destinationAddress() @@ -505,59 +86,6 @@ func (pg *Page) HandleUserInteractions() { pg.parentWindow.Display(NewManualCoinSelectionPage(pg.Load, pg)) } } - - if pg.nextButton.Clicked() { - if pg.selectedWallet.IsUnsignedTxExist() { - pg.confirmTxModal = newSendConfirmModal(pg.Load, pg.authoredTxData, *pg.selectedWallet) - pg.confirmTxModal.exchangeRateSet = pg.exchangeRate != -1 && pg.usdExchangeSet - pg.confirmTxModal.txLabel = pg.txLabelInputEditor.Editor.Text() - - pg.confirmTxModal.txSent = func() { - pg.resetFields() - pg.clearEstimates() - } - - pg.parentWindow.ShowModal(pg.confirmTxModal) - } - } - - // if destination switch is equal to Address - if pg.sendDestination.sendToAddress { - if pg.sendDestination.validate() { - if !components.IsFetchExchangeRateAPIAllowed(pg.WL) { - if len(pg.amount.amountEditor.Editor.Text()) == 0 { - pg.amount.SendMax = false - } - } else { - if len(pg.amount.amountEditor.Editor.Text()) == 0 { - pg.amount.usdAmountEditor.Editor.SetText("") - pg.amount.SendMax = false - } - } - } - } else { - if !components.IsFetchExchangeRateAPIAllowed(pg.WL) { - if len(pg.amount.amountEditor.Editor.Text()) == 0 { - pg.amount.SendMax = false - } - } else { - if len(pg.amount.amountEditor.Editor.Text()) == 0 { - pg.amount.usdAmountEditor.Editor.SetText("") - pg.amount.SendMax = false - } - } - } - - if len(pg.amount.amountEditor.Editor.Text()) > 0 && pg.sourceAccountSelector.Changed() { - pg.amount.validateAmount() - pg.validateAndConstructTxAmountOnly() - } - - if pg.amount.IsMaxClicked() { - pg.amount.setError("") - pg.amount.SendMax = true - pg.amount.amountChanged() - } } // KeysToHandle returns an expression that describes a set of key combinations @@ -583,7 +111,3 @@ func (pg *Page) HandleKeyPress(_ *key.Event) {} func (pg *Page) OnNavigatedFrom() { pg.ctxCancel() // causes crash if nil, when the main page is closed if send page is created but never displayed (because sync in progress) } - -func (pg *Page) isFeerateAPIApproved() bool { - return pg.WL.AssetsManager.IsHTTPAPIPrivacyModeOff(libUtil.FeeRateHTTPAPI) -} diff --git a/ui/page/send/send_amount.go b/ui/page/send/send_amount.go index 7bc61a8c7..4c323ce69 100644 --- a/ui/page/send/send_amount.go +++ b/ui/page/send/send_amount.go @@ -11,15 +11,15 @@ import ( "github.com/crypto-power/cryptopower/libwallet/assets/dcr" libUtil "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/crypto-power/cryptopower/ui/cryptomaterial" - "github.com/crypto-power/cryptopower/ui/load" "github.com/crypto-power/cryptopower/ui/utils" "github.com/crypto-power/cryptopower/ui/values" "github.com/decred/dcrd/dcrutil/v4" ) type sendAmount struct { - *load.Load + theme *cryptomaterial.Theme + assetType libUtil.AssetType amountEditor cryptomaterial.Editor usdAmountEditor cryptomaterial.Editor @@ -33,14 +33,15 @@ type sendAmount struct { exchangeRate float64 } -func newSendAmount(l *load.Load) *sendAmount { +func newSendAmount(theme *cryptomaterial.Theme, assetType libUtil.AssetType) *sendAmount { sa := &sendAmount{ - Load: l, + theme: theme, exchangeRate: -1, + assetType: assetType, } - hit := fmt.Sprintf("%s (%s)", values.String(values.StrAmount), string(l.WL.SelectedWallet.Wallet.GetAssetType())) - sa.amountEditor = l.Theme.Editor(new(widget.Editor), hit) + hit := fmt.Sprintf("%s (%s)", values.String(values.StrAmount), string(assetType)) + sa.amountEditor = theme.Editor(new(widget.Editor), hit) sa.amountEditor.Editor.SetText("") sa.amountEditor.HasCustomButton = true sa.amountEditor.Editor.SingleLine = true @@ -49,7 +50,7 @@ func newSendAmount(l *load.Load) *sendAmount { sa.amountEditor.CustomButton.Text = values.String(values.StrMax) sa.amountEditor.CustomButton.CornerRadius = values.MarginPadding0 - sa.usdAmountEditor = l.Theme.Editor(new(widget.Editor), values.String(values.StrAmount)+" (USD)") + sa.usdAmountEditor = theme.Editor(new(widget.Editor), values.String(values.StrAmount)+" (USD)") sa.usdAmountEditor.Editor.SetText("") sa.usdAmountEditor.HasCustomButton = true sa.usdAmountEditor.Editor.SingleLine = true @@ -65,13 +66,13 @@ func newSendAmount(l *load.Load) *sendAmount { // styleWidgets sets the appropriate colors for the amount widgets. func (sa *sendAmount) styleWidgets() { - sa.amountEditor.CustomButton.Background = sa.Theme.Color.Gray1 - sa.amountEditor.CustomButton.Color = sa.Theme.Color.Surface - sa.amountEditor.EditorStyle.Color = sa.Theme.Color.Text + sa.amountEditor.CustomButton.Background = sa.theme.Color.Gray1 + sa.amountEditor.CustomButton.Color = sa.theme.Color.Surface + sa.amountEditor.EditorStyle.Color = sa.theme.Color.Text - sa.usdAmountEditor.CustomButton.Background = sa.Theme.Color.Gray1 - sa.usdAmountEditor.CustomButton.Color = sa.Theme.Color.Surface - sa.usdAmountEditor.EditorStyle.Color = sa.Theme.Color.Text + sa.usdAmountEditor.CustomButton.Background = sa.theme.Color.Gray1 + sa.usdAmountEditor.CustomButton.Color = sa.theme.Color.Surface + sa.usdAmountEditor.EditorStyle.Color = sa.theme.Color.Text } func (sa *sendAmount) setExchangeRate(exchangeRate float64) { @@ -84,7 +85,7 @@ func (sa *sendAmount) setAmount(amount int64) { // amount input to avoid construct tx cycle. sa.sendMaxChangeEvent = sa.SendMax amountSet := dcrutil.Amount(amount).ToCoin() - if sa.Load.WL.SelectedWallet.Wallet.GetAssetType() == libUtil.BTCWalletAsset { + if sa.assetType == libUtil.BTCWalletAsset { amountSet = btcutil.Amount(amount).ToBTC() } sa.amountEditor.Editor.SetText(fmt.Sprintf("%.8f", amountSet)) @@ -118,7 +119,7 @@ func (sa *sendAmount) validAmount() (int64, bool, error) { return -1, sa.SendMax, err } - if sa.Load.WL.SelectedWallet.Wallet.GetAssetType() == libUtil.BTCWalletAsset { + if sa.assetType == libUtil.BTCWalletAsset { return btc.AmountSatoshi(amount), sa.SendMax, nil } return dcr.AmountAtom(amount), sa.SendMax, nil @@ -201,19 +202,19 @@ func (sa *sendAmount) handle() { sa.amountEditor.SetError(sa.amountErrorText) if sa.amountErrorText != "" { - sa.amountEditor.LineColor = sa.Theme.Color.Danger - sa.usdAmountEditor.LineColor = sa.Theme.Color.Danger + sa.amountEditor.LineColor = sa.theme.Color.Danger + sa.usdAmountEditor.LineColor = sa.theme.Color.Danger } else { - sa.amountEditor.LineColor = sa.Theme.Color.Gray2 - sa.usdAmountEditor.LineColor = sa.Theme.Color.Gray2 + sa.amountEditor.LineColor = sa.theme.Color.Gray2 + sa.usdAmountEditor.LineColor = sa.theme.Color.Gray2 } if sa.SendMax { - sa.amountEditor.CustomButton.Background = sa.Theme.Color.Primary - sa.usdAmountEditor.CustomButton.Background = sa.Theme.Color.Primary + sa.amountEditor.CustomButton.Background = sa.theme.Color.Primary + sa.usdAmountEditor.CustomButton.Background = sa.theme.Color.Primary } else if len(sa.amountEditor.Editor.Text()) < 1 || !sa.SendMax { - sa.amountEditor.CustomButton.Background = sa.Theme.Color.Gray1 - sa.usdAmountEditor.CustomButton.Background = sa.Theme.Color.Gray1 + sa.amountEditor.CustomButton.Background = sa.theme.Color.Gray1 + sa.usdAmountEditor.CustomButton.Background = sa.theme.Color.Gray1 } for _, evt := range sa.amountEditor.Editor.Events() { @@ -258,3 +259,7 @@ func (sa *sendAmount) IsMaxClicked() bool { } return true } + +func (sa *sendAmount) setAssetType(assetType libUtil.AssetType) { + sa.assetType = assetType +} diff --git a/ui/page/send/send_destination.go b/ui/page/send/send_destination.go index af948763d..6d9696d46 100644 --- a/ui/page/send/send_destination.go +++ b/ui/page/send/send_destination.go @@ -22,6 +22,7 @@ const ( type destination struct { *load.Load + selectedWallet sharedW.Asset addressChanged func() destinationAddressEditor cryptomaterial.Editor destinationAccountSelector *components.WalletAndAccountSelector @@ -33,7 +34,7 @@ type destination struct { selectedIndex int } -func newSendDestination(l *load.Load) *destination { +func newSendDestination(l *load.Load, selectedWallet sharedW.Asset) *destination { dst := &destination{ Load: l, } @@ -47,8 +48,14 @@ func newSendDestination(l *load.Load) *destination { {Text: values.String(values.StrWallets)}, }) + dst.initDestinationWalletSelector(selectedWallet) + + return dst +} + +func (dst *destination) initDestinationWalletSelector(selectedWallet sharedW.Asset) { // Destination wallet picker - dst.destinationWalletSelector = components.NewWalletAndAccountSelector(dst.Load, l.WL.SelectedWallet.Wallet.GetAssetType()). + dst.destinationWalletSelector = components.NewWalletAndAccountSelector(dst.Load, selectedWallet.GetAssetType()). EnableWatchOnlyWallets(true). Title(values.String(values.StrTo)) @@ -57,8 +64,7 @@ func newSendDestination(l *load.Load) *destination { EnableWatchOnlyWallets(true). Title(values.String(values.StrAccount)) dst.destinationAccountSelector.SelectFirstValidAccount(dst.destinationWalletSelector.SelectedWallet()) - - return dst + dst.selectedWallet = selectedWallet } // destinationAddress validates the destination address obtained from the provided @@ -95,7 +101,7 @@ func (dst *destination) validateDestinationAddress() (string, error) { return address, fmt.Errorf(values.String(values.StrDestinationMissing)) } - if dst.WL.SelectedWallet.Wallet.IsAddressValid(address) { + if dst.selectedWallet.IsAddressValid(address) { dst.destinationAddressEditor.SetError("") return address, nil } diff --git a/ui/page/send/send_modal.go b/ui/page/send/send_modal.go index db5333ea0..79c736278 100644 --- a/ui/page/send/send_modal.go +++ b/ui/page/send/send_modal.go @@ -3,12 +3,11 @@ package send import ( "context" - "gioui.org/font" + "gioui.org/io/key" "gioui.org/layout" "github.com/crypto-power/cryptopower/ui/cryptomaterial" "github.com/crypto-power/cryptopower/ui/load" - "github.com/crypto-power/cryptopower/ui/page/components" "github.com/crypto-power/cryptopower/ui/values" ) @@ -16,40 +15,27 @@ type PageModal struct { *load.Load *cryptomaterial.Modal - sendPage *Page - - ctx context.Context // page context + ctx context.Context // modal context ctxCancel context.CancelFunc - okBtn cryptomaterial.Button - - sourceWalletSelector *components.WalletAndAccountSelector + *widgetInitializer } func NewPageModal(l *load.Load) *PageModal { sm := &PageModal{ - Load: l, - Modal: l.Theme.ModalFloatTitle(values.String(values.StrSettings)), + Load: l, + Modal: l.Theme.ModalFloatTitle(values.String(values.StrSend)), + widgetInitializer: newWidgetInitializer(l, true), } - sm.okBtn = l.Theme.Button(values.String(values.StrOK)) - sm.okBtn.Font.Weight = font.Medium - - // initialize wallet selector - sm.sourceWalletSelector = components.NewWalletAndAccountSelector(sm.Load). - Title(values.String(values.StrSelectWallet)) - sm.setSelectedWallet() - - sm.sendPage = NewSendPage(sm.Load) - - sm.initWalletSelectors() - return sm } func (sm *PageModal) OnResume() { sm.ctx, sm.ctxCancel = context.WithCancel(context.TODO()) - sm.sourceWalletSelector.ListenForTxNotifications(sm.ctx, sm.ParentWindow()) + sm.sourceAccountSelector.ListenForTxNotifications(sm.ctx, sm.ParentWindow()) + + sm.onLoaded() } func (sm *PageModal) OnDismiss() { @@ -57,39 +43,26 @@ func (sm *PageModal) OnDismiss() { } func (sm *PageModal) Handle() { - if sm.okBtn.Clicked() || sm.Modal.BackdropClicked(true) { + if sm.Modal.BackdropClicked(true) { sm.Dismiss() } - sm.sendPage.HandleUserInteractions() + + sm.handleFunc() } func (sm *PageModal) Layout(gtx C) D { - walletSelector := func(gtx C) D { - return sm.sourceWalletSelector.Layout(sm.ParentWindow(), gtx) - } - PageModalLayout := []layout.Widget{ + modalContent := []layout.Widget{ func(gtx C) D { - return sm.sendPage.layoutDesktop(sm.ParentWindow(), walletSelector, gtx) + return sm.layoutDesktop(gtx, sm.ParentWindow()) }, } - return sm.Modal.Layout(gtx, PageModalLayout, 450) -} - -func (sm *PageModal) initWalletSelectors() { - // Source wallet picker - sm.sourceWalletSelector.WalletSelected(func(selectedWallet *load.WalletMapping) { - sm.setSelectedWallet() - sm.sendPage.sourceAccountSelector.SelectFirstValidAccount(selectedWallet) - }) - sm.setSelectedWallet() + return sm.Modal.Layout(gtx, modalContent, 450) } -func (sm *PageModal) setSelectedWallet() { - sm.WL.SelectedWallet = &load.WalletItem{ - Wallet: sm.sourceWalletSelector.SelectedWallet().Asset, - } - balance, err := sm.WL.TotalWalletsBalance() - if err == nil { - sm.WL.SelectedWallet.TotalBalance = balance.String() - } +// KeysToHandle returns an expression that describes a set of key combinations +// that this page wishes to capture. The HandleKeyPress() method will only be +// called when any of these key combinations is pressed. +// Satisfies the load.KeyEventHandler interface for receiving key events. +func (sm *PageModal) KeysToHandle() key.Set { + return cryptomaterial.AnyKeyWithOptionalModifier(key.ModShift, key.NameTab) } diff --git a/ui/page/send/shared_widgets.go b/ui/page/send/shared_widgets.go new file mode 100644 index 000000000..023cdbd38 --- /dev/null +++ b/ui/page/send/shared_widgets.go @@ -0,0 +1,580 @@ +package send + +import ( + "fmt" + "strings" + + "gioui.org/layout" + "gioui.org/widget" + + "github.com/crypto-power/cryptopower/app" + sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" + libUtil "github.com/crypto-power/cryptopower/libwallet/utils" + "github.com/crypto-power/cryptopower/ui/cryptomaterial" + "github.com/crypto-power/cryptopower/ui/load" + "github.com/crypto-power/cryptopower/ui/modal" + "github.com/crypto-power/cryptopower/ui/page/components" + "github.com/crypto-power/cryptopower/ui/utils" + "github.com/crypto-power/cryptopower/ui/values" +) + +const ( + // MaxTxLabelSize defines the maximum number of characters to be allowed on + // txLabelInputEditor component. + MaxTxLabelSize = 100 +) + +var ( + automaticCoinSelection = values.String(values.StrAutomatic) + manualCoinSelection = values.String(values.StrManual) +) + +type widgetInitializer struct { + *load.Load + + parentWindow app.WindowNavigator + pageContainer *widget.List + + sourceWalletSelector *components.WalletAndAccountSelector + sourceAccountSelector *components.WalletAndAccountSelector + sendDestination *destination + amount *sendAmount + + infoButton cryptomaterial.IconButton + retryExchange cryptomaterial.Button + nextButton cryptomaterial.Button + + shadowBox *cryptomaterial.Shadow + backdrop *widget.Clickable + + isFetchingExchangeRate bool + isModalLayout bool + + exchangeRate float64 + usdExchangeSet bool + exchangeRateMessage string + confirmTxModal *sendConfirmModal + currencyExchange string + + txLabelInputEditor cryptomaterial.Editor + + *authoredTxData + selectedWallet *load.WalletMapping + feeRateSelector *components.FeeRateSelector + + toCoinSelection *cryptomaterial.Clickable + + selectedUTXOs selectedUTXOsInfo +} + +type authoredTxData struct { + destinationAddress string + destinationAccount *sharedW.Account + sourceAccount *sharedW.Account + txFee string + txFeeUSD string + totalCost string + totalCostUSD string + balanceAfterSend string + balanceAfterSendUSD string + sendAmount string + sendAmountUSD string +} + +type selectedUTXOsInfo struct { + sourceAccount *sharedW.Account + selectedUTXOs []*sharedW.UnspentOutput + totalUTXOsAmount int64 +} + +func newWidgetInitializer(l *load.Load, isModalLayout bool) *widgetInitializer { + wi := &widgetInitializer{ + Load: l, + exchangeRate: -1, + + authoredTxData: &authoredTxData{}, + shadowBox: l.Theme.Shadow(), + backdrop: new(widget.Clickable), + isModalLayout: isModalLayout, + } + + if isModalLayout { + wi.initWalletSelector() + } else { + wi.selectedWallet = &load.WalletMapping{ + Asset: l.WL.SelectedWallet.Wallet, + } + } + + wi.amount = newSendAmount(l.Theme, wi.selectedWallet.GetAssetType()) + wi.sendDestination = newSendDestination(l, wi.selectedWallet.Asset) + + callbackFunc := func() libUtil.AssetType { + return wi.selectedWallet.GetAssetType() + } + wi.feeRateSelector = components.NewFeeRateSelector(l, callbackFunc).ShowSizeAndCost() + wi.feeRateSelector.TitleInset = layout.Inset{Bottom: values.MarginPadding10} + wi.feeRateSelector.ContainerInset = layout.Inset{Bottom: values.MarginPadding100} + wi.feeRateSelector.WrapperInset = layout.UniformInset(values.MarginPadding15) + + wi.initializeAccountSelectors() + + wi.sendDestination.addressChanged = func() { + wi.validateAndConstructTx() + } + + wi.amount.amountChanged = func() { + wi.validateAndConstructTxAmountOnly() + } + + wi.pageContainer = &widget.List{ + List: layout.List{ + Axis: layout.Vertical, + Alignment: layout.Middle, + }, + } + + buttonInset := layout.Inset{ + Top: values.MarginPadding4, + Right: values.MarginPadding8, + Bottom: values.MarginPadding4, + Left: values.MarginPadding8, + } + + wi.nextButton = wi.Theme.Button(values.String(values.StrNext)) + wi.nextButton.TextSize = values.TextSize18 + wi.nextButton.Inset = layout.Inset{Top: values.MarginPadding15, Bottom: values.MarginPadding15} + wi.nextButton.SetEnabled(false) + + _, wi.infoButton = components.SubpageHeaderButtons(wi.Load) + + wi.retryExchange = wi.Theme.Button(values.String(values.StrRetry)) + wi.retryExchange.Background = wi.Theme.Color.Gray1 + wi.retryExchange.Color = wi.Theme.Color.Surface + wi.retryExchange.TextSize = values.TextSize12 + wi.retryExchange.Inset = buttonInset + + wi.txLabelInputEditor = wi.Theme.Editor(new(widget.Editor), values.String(values.StrNote)) + wi.txLabelInputEditor.Editor.SingleLine = false + wi.txLabelInputEditor.Editor.SetText("") + // Set the maximum characters the editor can accept. + wi.txLabelInputEditor.Editor.MaxLen = MaxTxLabelSize + + wi.toCoinSelection = wi.Theme.NewClickable(false) + + return wi +} + +// RestyleWidgets restyles select widgets to match the current theme. This is +// especially necessary when the dark mode setting is changed. +func (wi *widgetInitializer) restyleWidgets() { + wi.amount.styleWidgets() + wi.sendDestination.styleWidgets() +} + +func (wi *widgetInitializer) onLoaded() { + wi.restyleWidgets() + + if !wi.selectedWallet.Asset.IsSynced() { + // Events are disabled until the wallet is fully synced. + return + } + + // destinationAccountSelector does not have a default value, + // so assign it an initial value here + wi.sendDestination.destinationAccountSelector.SelectFirstValidAccount(wi.sendDestination.destinationWalletSelector.SelectedWallet()) + wi.sendDestination.destinationAddressEditor.Editor.Focus() + + wi.usdExchangeSet = false + if components.IsFetchExchangeRateAPIAllowed(wi.WL) { + wi.currencyExchange = wi.WL.AssetsManager.GetCurrencyConversionExchange() + wi.usdExchangeSet = true + go wi.fetchExchangeRate() + } else { + // If exchange rate is not supported, validate and construct the TX. + wi.validateAndConstructTx() + } + + if wi.selectedWallet.GetAssetType() == libUtil.BTCWalletAsset && wi.isFeerateAPIApproved() { + // This API call may take sometime to return. Call this before and cache + // results. + go wi.selectedWallet.GetAPIFeeRate() + } +} + +// initWalletSelector is used for the send modal to for wallet selection. +func (wi *widgetInitializer) initWalletSelector() { + // initialize wallet selector + wi.sourceWalletSelector = components.NewWalletAndAccountSelector(wi.Load). + Title(values.String(values.StrSelectWallet)) + wi.selectedWallet = wi.sourceWalletSelector.SelectedWallet() + + // Source wallet picker + wi.sourceWalletSelector.WalletSelected(func(selectedWallet *load.WalletMapping) { + wi.selectedWallet = selectedWallet + wi.initializeAccountSelectors() + wi.amount.setAssetType(wi.selectedWallet.GetAssetType()) + wi.sendDestination.initDestinationWalletSelector(selectedWallet) + }) +} + +func (wi *widgetInitializer) initializeAccountSelectors() { + // Source account picker + wi.sourceAccountSelector = components.NewWalletAndAccountSelector(wi.Load). + Title(values.String(values.StrFrom)). + AccountSelected(func(selectedAccount *sharedW.Account) { + // this resets the selected destination account based on the + // selected source account. This is done to prevent sending to + // an account that is invalid either because the destination + // account is the same as the source account or because the + // destination account needs to change based on if the selected + // wallet has privacy enabled. + wi.sendDestination.destinationAccountSelector.SelectFirstValidAccount( + wi.sendDestination.destinationWalletSelector.SelectedWallet()) + wi.validateAndConstructTx() + }). + AccountValidator(func(account *sharedW.Account) bool { + accountIsValid := account.Number != load.MaxInt32 && !wi.selectedWallet.IsWatchingOnlyWallet() + + if wi.selectedWallet.ReadBoolConfigValueForKey(sharedW.AccountMixerConfigSet, false) && + !wi.selectedWallet.ReadBoolConfigValueForKey(sharedW.SpendUnmixedFundsKey, false) { + // Spending unmixed fund isn't permitted for the selected wallet + + // only mixed accounts can send to address/wallets for wallet with privacy setup + switch wi.sendDestination.accountSwitch.SelectedIndex() { + case sendToAddress: + accountIsValid = account.Number == wi.selectedWallet.MixedAccountNumber() + case SendToWallet: + destinationWalletID := wi.sendDestination.destinationWalletSelector.SelectedWallet().GetWalletID() + if destinationWalletID != wi.selectedWallet.GetWalletID() { + accountIsValid = account.Number == wi.selectedWallet.MixedAccountNumber() + } + } + } + return accountIsValid + }). + SetActionInfoText(values.String(values.StrTxConfModalInfoTxt)) + + // if a source account exists, don't overwrite it. + if wi.sourceAccountSelector.SelectedAccount() == nil { + wi.sourceAccountSelector.SelectFirstValidAccount(wi.selectedWallet) + } + + wi.sendDestination.destinationAccountSelector = wi.sendDestination.destinationAccountSelector.AccountValidator(func(account *sharedW.Account) bool { + accountIsValid := account.Number != load.MaxInt32 + // Filter mixed wallet + destinationWallet := wi.sendDestination.destinationAccountSelector.SelectedWallet() + isMixedAccount := destinationWallet.MixedAccountNumber() == account.Number + // Filter the sending account. + sourceWalletID := wi.sourceAccountSelector.SelectedAccount().WalletID + isSameAccount := sourceWalletID == account.WalletID && account.Number == wi.sourceAccountSelector.SelectedAccount().Number + if !accountIsValid || isSameAccount || isMixedAccount { + return false + } + return true + }) + + wi.sendDestination.destinationAccountSelector.AccountSelected(func(selectedAccount *sharedW.Account) { + wi.validateAndConstructTx() + }) + + wi.sendDestination.destinationWalletSelector.WalletSelected(func(selectedWallet *load.WalletMapping) { + wi.sendDestination.destinationAccountSelector.SelectFirstValidAccount(selectedWallet) + if wi.selectedWallet.Asset.GetAssetType() == libUtil.DCRWalletAsset { + wi.sourceAccountSelector.SelectFirstValidAccount(wi.selectedWallet) + } + }) +} + +func (wi *widgetInitializer) updateSelectedUTXOs(utxos []*sharedW.UnspentOutput) { + wi.selectedUTXOs = selectedUTXOsInfo{ + selectedUTXOs: utxos, + sourceAccount: wi.sourceAccountSelector.SelectedAccount(), + } + if len(utxos) > 0 { + for _, elem := range utxos { + wi.selectedUTXOs.totalUTXOsAmount += elem.Amount.ToInt() + } + } +} + +func (wi *widgetInitializer) fetchExchangeRate() { + if wi.isFetchingExchangeRate { + return + } + wi.isFetchingExchangeRate = true + var market string + switch wi.selectedWallet.Asset.GetAssetType() { + case libUtil.DCRWalletAsset: + market = values.DCRUSDTMarket + case libUtil.BTCWalletAsset: + market = values.BTCUSDTMarket + case libUtil.LTCWalletAsset: + market = values.LTCUSDTMarket + default: + log.Errorf("Unsupported asset type: %s", wi.selectedWallet.Asset.GetAssetType()) + wi.isFetchingExchangeRate = false + return + } + + rate, err := wi.WL.AssetsManager.ExternalService.GetTicker(wi.currencyExchange, market) + if err != nil { + log.Error(err) + wi.isFetchingExchangeRate = false + return + } + + wi.exchangeRate = rate.LastTradePrice + wi.amount.setExchangeRate(wi.exchangeRate) + wi.validateAndConstructTx() // convert estimates to usd + + wi.isFetchingExchangeRate = false + wi.parentWindow.Reload() +} + +func (wi *widgetInitializer) validateAndConstructTx() { + // delete all the previous errors set earlier. + wi.amountValidationError("") + wi.addressValidationError("") + + if wi.validate() { + wi.constructTx() + } else { + wi.clearEstimates() + wi.showBalaceAfterSend() + } +} + +func (wi *widgetInitializer) validateAndConstructTxAmountOnly() { + defer wi.RefreshTheme(wi.parentWindow) + + if !wi.sendDestination.validate() && wi.amount.amountIsValid() { + wi.constructTx() + } else { + wi.validateAndConstructTx() + } +} + +func (wi *widgetInitializer) validate() bool { + amountIsValid := wi.amount.amountIsValid() + addressIsValid := wi.sendDestination.validate() + + // No need for checking the err message since it is as result of amount and + // address validation. + // validForSending + return amountIsValid && addressIsValid +} + +func (wi *widgetInitializer) constructTx() { + destinationAddress, err := wi.sendDestination.destinationAddress() + if err != nil { + wi.addressValidationError(err.Error()) + return + } + destinationAccount := wi.sendDestination.destinationAccount() + + amountAtom, SendMax, err := wi.amount.validAmount() + if err != nil { + wi.amountValidationError(err.Error()) + return + } + + sourceAccount := wi.sourceAccountSelector.SelectedAccount() + selectedUTXOs := make([]*sharedW.UnspentOutput, 0) + if sourceAccount == wi.selectedUTXOs.sourceAccount { + selectedUTXOs = wi.selectedUTXOs.selectedUTXOs + } + + err = wi.selectedWallet.NewUnsignedTx(sourceAccount.Number, selectedUTXOs) + if err != nil { + wi.amountValidationError(err.Error()) + return + } + + err = wi.selectedWallet.AddSendDestination(destinationAddress, amountAtom, SendMax) + if err != nil { + if strings.Contains(err.Error(), "amount") { + wi.amountValidationError(err.Error()) + return + } + wi.addressValidationError(err.Error()) + return + } + + feeAndSize, err := wi.selectedWallet.EstimateFeeAndSize() + if err != nil { + wi.amountValidationError(err.Error()) + return + } + + feeAtom := feeAndSize.Fee.UnitValue + spendableAmount := sourceAccount.Balance.Spendable.ToInt() + if len(selectedUTXOs) > 0 { + spendableAmount = wi.selectedUTXOs.totalUTXOsAmount + } + + if SendMax { + amountAtom = spendableAmount - feeAtom + } + + wal := wi.selectedWallet.Asset + totalSendingAmount := wal.ToAmount(amountAtom + feeAtom) + balanceAfterSend := wal.ToAmount(spendableAmount - totalSendingAmount.ToInt()) + + // populate display data + wi.txFee = wal.ToAmount(feeAtom).String() + + wi.feeRateSelector.EstSignedSize = fmt.Sprintf("%d Bytes", feeAndSize.EstimatedSignedSize) + wi.feeRateSelector.TxFee = wi.txFee + wi.feeRateSelector.SetFeerate(feeAndSize.FeeRate) + wi.totalCost = totalSendingAmount.String() + wi.balanceAfterSend = balanceAfterSend.String() + wi.sendAmount = wal.ToAmount(amountAtom).String() + wi.destinationAddress = destinationAddress + wi.destinationAccount = destinationAccount + wi.sourceAccount = sourceAccount + + if SendMax { + // TODO: this workaround ignores the change events from the + // amount input to avoid construct tx cycle. + wi.amount.setAmount(amountAtom) + } + + if wi.exchangeRate != -1 && wi.usdExchangeSet { + wi.feeRateSelector.USDExchangeSet = true + wi.txFeeUSD = fmt.Sprintf("$%.4f", utils.CryptoToUSD(wi.exchangeRate, feeAndSize.Fee.CoinValue)) + wi.feeRateSelector.TxFeeUSD = wi.txFeeUSD + wi.totalCostUSD = utils.FormatUSDBalance(wi.Printer, utils.CryptoToUSD(wi.exchangeRate, totalSendingAmount.ToCoin())) + wi.balanceAfterSendUSD = utils.FormatUSDBalance(wi.Printer, utils.CryptoToUSD(wi.exchangeRate, balanceAfterSend.ToCoin())) + + usdAmount := utils.CryptoToUSD(wi.exchangeRate, wal.ToAmount(amountAtom).ToCoin()) + wi.sendAmountUSD = utils.FormatUSDBalance(wi.Printer, usdAmount) + } +} + +func (wi *widgetInitializer) showBalaceAfterSend() { + if wi.sourceAccountSelector != nil { + sourceAccount := wi.sourceAccountSelector.SelectedAccount() + if sourceAccount.Balance == nil { + return + } + balanceAfterSend := sourceAccount.Balance.Spendable + wi.balanceAfterSend = balanceAfterSend.String() + wi.balanceAfterSendUSD = utils.FormatUSDBalance(wi.Printer, utils.CryptoToUSD(wi.exchangeRate, balanceAfterSend.ToCoin())) + } +} + +func (wi *widgetInitializer) amountValidationError(err string) { + wi.amount.setError(err) + wi.clearEstimates() +} + +func (wi *widgetInitializer) addressValidationError(err string) { + wi.sendDestination.setError(err) + wi.clearEstimates() +} + +func (wi *widgetInitializer) clearEstimates() { + wi.txFee = " - " + string(wi.selectedWallet.GetAssetType()) + wi.feeRateSelector.TxFee = wi.txFee + wi.txFeeUSD = " - " + wi.feeRateSelector.TxFeeUSD = wi.txFeeUSD + wi.totalCost = " - " + string(wi.selectedWallet.GetAssetType()) + wi.totalCostUSD = " - " + wi.balanceAfterSend = " - " + string(wi.selectedWallet.GetAssetType()) + wi.balanceAfterSendUSD = " - " + wi.sendAmount = " - " + wi.sendAmountUSD = " - " + wi.feeRateSelector.SetFeerate(0) +} + +func (wi *widgetInitializer) resetFields() { + wi.sendDestination.clearAddressInput() + wi.txLabelInputEditor.Editor.SetText("") + + wi.amount.resetFields() +} + +func (wi *widgetInitializer) isFeerateAPIApproved() bool { + return wi.WL.AssetsManager.IsHTTPAPIPrivacyModeOff(libUtil.FeeRateHTTPAPI) +} + +func (wi *widgetInitializer) handleFunc() { + if wi.feeRateSelector.FetchRates.Clicked() { + go wi.feeRateSelector.FetchFeeRate(wi.parentWindow, wi.selectedWallet) + } + + if wi.feeRateSelector.EditRates.Clicked() { + wi.feeRateSelector.OnEditRateClicked(wi.selectedWallet) + } + + wi.nextButton.SetEnabled(wi.validate()) + wi.sendDestination.handle() + wi.amount.handle() + + if wi.infoButton.Button.Clicked() { + textWithUnit := values.String(values.StrSend) + " " + string(wi.selectedWallet.GetAssetType()) + info := modal.NewCustomModal(wi.Load). + Title(textWithUnit). + Body(values.String(values.StrSendInfo)). + SetPositiveButtonText(values.String(values.StrGotIt)) + wi.parentWindow.ShowModal(info) + } + + if wi.retryExchange.Clicked() { + go wi.fetchExchangeRate() + } + + if wi.nextButton.Clicked() { + if wi.selectedWallet.IsUnsignedTxExist() { + wi.confirmTxModal = newSendConfirmModal(wi.Load, wi.authoredTxData, *wi.selectedWallet) + wi.confirmTxModal.exchangeRateSet = wi.exchangeRate != -1 && wi.usdExchangeSet + wi.confirmTxModal.txLabel = wi.txLabelInputEditor.Editor.Text() + + wi.confirmTxModal.txSent = func() { + wi.resetFields() + wi.clearEstimates() + } + + wi.parentWindow.ShowModal(wi.confirmTxModal) + } + } + + // if destination switch is equal to Address + if wi.sendDestination.sendToAddress { + if wi.sendDestination.validate() { + if !components.IsFetchExchangeRateAPIAllowed(wi.WL) { + if len(wi.amount.amountEditor.Editor.Text()) == 0 { + wi.amount.SendMax = false + } + } else { + if len(wi.amount.amountEditor.Editor.Text()) == 0 { + wi.amount.usdAmountEditor.Editor.SetText("") + wi.amount.SendMax = false + } + } + } + } else { + if !components.IsFetchExchangeRateAPIAllowed(wi.WL) { + if len(wi.amount.amountEditor.Editor.Text()) == 0 { + wi.amount.SendMax = false + } + } else { + if len(wi.amount.amountEditor.Editor.Text()) == 0 { + wi.amount.usdAmountEditor.Editor.SetText("") + wi.amount.SendMax = false + } + } + } + + if len(wi.amount.amountEditor.Editor.Text()) > 0 && wi.sourceAccountSelector.Changed() { + wi.amount.validateAmount() + wi.validateAndConstructTxAmountOnly() + } + + if wi.amount.IsMaxClicked() { + wi.amount.setError("") + wi.amount.SendMax = true + wi.amount.amountChanged() + } +}