New design (#2797)

* refactor node form UI to use new ListItem model

* add new list item row types and widgets

* add style wrapper

* remove print [skip ci]

* Integrate new design mockup with ViewModel (#2653)

* feat: add Lightning Network support for Bitcoin wallets

* refactor: rename `fiatConvertationStore` to `fiatConversionStore` for consistency and update related occurrences across codebase

* feat: enhance address validation with Lightning Network invoice support for BTC & refactor wallet type/token checks in view model

* feat: add support for Lightning invoice detection, refactor MWEB deposit/withdraw actions, and integrate Lightning transaction creation with updated priority handling

* feat: add method to retrieve unused Spark deposit address for Bitcoin wallets

* feat: add Breez API key support and update secrets handling for Bitcoin Lightning wallet integration in workflows

* chore: update Breez SDK dependency to version 0.3.4 in pubspec files

* Add bitcoin secrets config [skip ci]

* feat: extend Lightning wallet functionality with transaction history fetching

* feat: add LNURL-pay address detection and support in address parsing flow for Bitcoin Lightning integration

* refactor: simplify `ReceivePageOption` logic

* refactor: centralize `PaymentURI` generation logic across wallet types

* feat: enhance `PaymentURI` handling with asynchronous support and Lightning-specific functionality

* refactor: streamline `PaymentURI` logic and remove redundant URI implementations across wallet types

* refactor: remove redundant debug print statement from `bitcoin_wallet_addresses.dart`

* refactor: improve consistency in widget styling and centralized label logic, add Bitcoin Lightning deposit/withdraw support

* feat: reload balance and tx history after sending a lightning transaction

* feat: improve address formatting for human-readable addresses and update the default LNURL domain

* fix: merge conflicts

* feat: add error handling for LightningWallet initialization and adjust transaction direction logic

* integrate homepage from new ui mockup

* fix import

* feat: add Lightning Network support for Bitcoin wallets

* refactor: rename `fiatConvertationStore` to `fiatConversionStore` for consistency and update related occurrences across codebase

* feat: enhance address validation with Lightning Network invoice support for BTC & refactor wallet type/token checks in view model

* feat: add support for Lightning invoice detection, refactor MWEB deposit/withdraw actions, and integrate Lightning transaction creation with updated priority handling

* feat: add method to retrieve unused Spark deposit address for Bitcoin wallets

* feat: add Breez API key support and update secrets handling for Bitcoin Lightning wallet integration in workflows

* chore: update Breez SDK dependency to version 0.3.4 in pubspec files

* Add bitcoin secrets config [skip ci]

* feat: extend Lightning wallet functionality with transaction history fetching

* feat: add LNURL-pay address detection and support in address parsing flow for Bitcoin Lightning integration

* refactor: simplify `ReceivePageOption` logic

* refactor: centralize `PaymentURI` generation logic across wallet types

* feat: enhance `PaymentURI` handling with asynchronous support and Lightning-specific functionality

* refactor: streamline `PaymentURI` logic and remove redundant URI implementations across wallet types

* refactor: remove redundant debug print statement from `bitcoin_wallet_addresses.dart`

* refactor: improve consistency in widget styling and centralized label logic, add Bitcoin Lightning deposit/withdraw support

* feat: reload balance and tx history after sending a lightning transaction

* feat: improve address formatting for human-readable addresses and update the default LNURL domain

* fix: merge conflicts

* feat: add error handling for LightningWallet initialization and adjust transaction direction logic

* integrate homepage from new ui mockup

* fix import

* add new-ui dir to pubspec_base

* minor layout fixes

* cleanup navbar logic

* Modify navbar behaviour

* smooth color change when selecting navbar options

* open old ui pages with new ui action buttons

* feat: working navbar in new ui

* fix: minor sizing tweaks

* CW-1266-integrate-bitcoin-lightning-through-spark-sdk (#2623)

* feat: add Lightning Network support for Bitcoin wallets

* refactor: rename `fiatConvertationStore` to `fiatConversionStore` for consistency and update related occurrences across codebase

* feat: enhance address validation with Lightning Network invoice support for BTC & refactor wallet type/token checks in view model

* feat: add support for Lightning invoice detection, refactor MWEB deposit/withdraw actions, and integrate Lightning transaction creation with updated priority handling

* feat: add method to retrieve unused Spark deposit address for Bitcoin wallets

* feat: add Breez API key support and update secrets handling for Bitcoin Lightning wallet integration in workflows

* chore: update Breez SDK dependency to version 0.3.4 in pubspec files

* Add bitcoin secrets config [skip ci]

* feat: extend Lightning wallet functionality with transaction history fetching

* feat: add LNURL-pay address detection and support in address parsing flow for Bitcoin Lightning integration

* refactor: simplify `ReceivePageOption` logic

* refactor: centralize `PaymentURI` generation logic across wallet types

* feat: enhance `PaymentURI` handling with asynchronous support and Lightning-specific functionality

* refactor: streamline `PaymentURI` logic and remove redundant URI implementations across wallet types

* refactor: remove redundant debug print statement from `bitcoin_wallet_addresses.dart`

* refactor: improve consistency in widget styling and centralized label logic, add Bitcoin Lightning deposit/withdraw support

* feat: reload balance and tx history after sending a lightning transaction

* feat: improve address formatting for human-readable addresses and update the default LNURL domain

* fix: merge conflicts

* feat: add error handling for LightningWallet initialization and adjust transaction direction logic

* feat: enable private transactions by default in LightningWallet and update Breez SDK version to 0.4.2

* minor fixes [skip ci]

* chore: fix some minor issues in comments (#2654)

Signed-off-by: black5box <black5box@outlook.com>

* fix: handle send-all functionality for LightningWallet transactions and adjust amount calculation logic

* fix-german (#2659)

* chore: update German localization strings for consistency and accuracy

* chore: update German localization strings for consistency and accuracy [skip-ci]

* feat: add LNURL support for address validation and LightningWallet compatibility, enhance error handling for OpenCryptoPay

* fix: adjust LightningWallet amount parsing from 9 to 8 decimal places

* feat: add LNURL support in LightningPaymentRequest and LightningWallet

* Fix navigation gradient (#2657)

* Fix navigation gradient

* Fix CONFIG_ARGS formatting in app_config.sh (#2660)

* fix: block wrongly parsed addresses (#2656)

* fix: block wrongly parsed addresses

* fix: move parsed address check into handlePaymentFlow method

* fix: Handle QR URLs separately for pay anything flow

* feat: add electrum seed support for Lightning

* refactor: improve `parseFixed` logic and add comprehensive unit tests for edge cases (#2661)

* Update cw_bitcoin/lib/bitcoin_wallet.dart [skip ci]

* resolve conflict issue

---------

Signed-off-by: black5box <black5box@outlook.com>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Co-authored-by: black5box <black5box@outlook.com>
Co-authored-by: tuxsudo <tuxsudo@tux.pizza>
Co-authored-by: cyan <cyjan@mrcyjanek.net>
Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com>

* remove old code [skip ci]

* fix: page widgets rebuilt every time

* chore: formatting

* remove unused navbar

* fix: lightning balance on balance card in lightning mode

* fix: return if uri == null

* feat: trade history in new ui

* feat: enhance balance model with secondary asset handling

* feat: unique card colors and icons for coins

* wip: card customization

* fix: merge conflict

* feat: customizable balance card

* Fix some spacings and test sizing

* Fix formatting

* feat: add Lightning Network support for send flow and fee handling adjustments

* refactor: remove unnecessary debug print statement in send flow for Lightning Network balance

* feat: integrate OpenCryptoPay QR handling in send flow and improve scan action handling logic

* feat: add maxDepositClaimFee parameter for Lightning wallet configuration

* Switch balance card to RoundedSuperellipse and set color list to Wrap

* Fix

* Switch to scroll view

* refactor: update navigation route from `receive` to `addressPage` in coin action row

* feat: bloc-based viewmodel for card customizer

* feat: mobx-independent balance card customizer vm

* minor fixes

* copyWith constructor for cleaner state changes

* refactor: wrap `HistorySection` with `Observer` for reactive state updates, update navigation for "Buy" action, and fix incorrect balance property

* feat: enhance Bitcoin receive flow with Lightning and SegWit options, update address page registration, and localize coin action labels

* feat: add helper methods for Lightning and SegWit receive page options in Bitcoin configuration

* feat: add Lightning transaction type handling and enable navigation to transaction details from history

* Add more special card types

* feat: new ui settings page

* feat: add Lightning actions to balance cards, refactor card rendering logic

* refactor: update settings page localization, hide unused settings items

* feat: add secrets generation for cw_bitcoin, including breezApiKey

* Lightning switcher animation (#2725)

* Properly animate bitcoin/lightning switcher

* Cleanup and fix sizing

* Final cleanup

* Fix action button colors (#2732)

* feat: add hardware wallet type handling and improve parsed address reset flow

* Refactor settings page to use generic row items (#2745)

* fix android back button in settings on android, force iphone-style transitions on all platforms

* move settings page to generic row impl

* fix imports

* Balance card fixes (#2729)

* Update balance card sizing

* Fix spacing

* Initial font changes (#2731)

* Initial font changes

* More font changes

* feat: integrate Lightning transaction support and improve transaction history UX

- Enhanced dashboard to compute confirmations using the first and last transactions.
- Updated `BitcoinWallet` to subscribe for and manage Lightning transactions.
- Improved `HistoryTile` layout and introduced new leading icons for Lightning transactions.

* feat: add unclaimed deposit actions and enhance Lightning event handling

* release changes [skip ci]

* Enhanced card customizer (#2744)

* enhanced card customizer

* Fix conflicts

* Add missing icons + adjust spacing

---------

Co-authored-by: tuxsudo <tuxsudo@tux.pizza>

* feat: enhance Lightning invoice handling and address validation

- Added support for identifying non-zero amount Lightning invoices.
- Refined invoice matching with updated regex patterns.
- Improved UI to conditionally display addresses in sending confirmations.
- Adjusted logic to handle zero-value Lightning invoices.

* feat: add `isPayjoinAvailable` computed property and update Payjoin visibility logic

* feat: improve Lightning wallet handling and block explorer logic

* update patch version [skip ci]

---------

Signed-off-by: black5box <black5box@outlook.com>
Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
Co-authored-by: tuxsudo <tuxsudo@tux.pizza>
Co-authored-by: black5box <black5box@outlook.com>
Co-authored-by: cyan <cyjan@mrcyjanek.net>
Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com>

* minor new ui fixes

* padding for new row separators

* remove debug prints

* New design improve nav bar (#2763)

* tweak navigation bar touch targets and sizing

* feat: add tests for identifying zero and non-zero amount Lightning invoices (#2772)

* feat: add tests for identifying zero and non-zero amount Lightning invoices

- Added unit tests to validate recognition of Bolt11 zero-amount and non-zero-amount invoices.
- Enhanced `isBolt11ZeroInvoice` function to handle `lightning:` prefixed invoices.

* fix: correct currency selection logic for Lightning wallets in Send ViewModel

* feat: improve Lightning wallet initialization and address generation

- Updated `init` method in `LightningWallet` to return a boolean for initialization success, adding error handling for failed setups.
- Integrated dynamic name generation using `generateName` for Lightning wallet addresses.

* refactor: relocate `generate_name` to `cw_core` and update imports

* chore: update breez sdk (#2789)

* Wire tokens and NFTs to new Home page (#2804)

* wiring for assets on home page

* fix imports

* wire nft page in new ui

* fix nft display in new ui

* fix totalWidth calc

* add tokens button + bugfixes

* add tokens button + bugfixes

* fix late initialization

* fix history section exception

* fixes

* Add haptic feedback (#2813)

* Add haptic feedback

* Add haptic feedback to correct ActionButton

* Return bottom gradient (#2808)

* Add IgnorePointer to bottom nav gradient (#2822)

* New design send page (#2791)

* new ui send page (wip)

* coin control page improvements

* fee settings, new send confirmation modal

* minor aesthetic fixes

* load network name without cw_bitcoin import

* recipient dot cleanup

* minor logic fixes

* ui polish

* - add aliaspay support
- fix wording for max button
- fix back button action in modals

* Sizing changes

* dismiss transaction on modal close

* sizing fix

* - fix remove recipient icon
- properly round amounts
- fix coin control page spacing

* - fix remove recipient icon
- properly round amounts
- fix coin control page spacing

* - fix button sizing
- add description for fees page

* Update lib/view_model/unspent_coins/unspent_coins_list_view_model.dart [skip ci]

* Update lib/view_model/send/send_view_model.dart [skip ci]

---------

Co-authored-by: tuxsudo <tuxsudo@tux.pizza>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Fix conflicts with dev

* Fix conflicts with dev

* Add zcash receive options to new design flow

* wire in missing settings in new ui

* up migration version

* formatting

* translations

* lint.sh

* New design receive page (#2834)

* feat: viewmodel on new receive page (wip)

* feat: address type selector for new ui receive page

* fix: minor fixes to touch targets and tap anims

* minor ui fix

* wip receive page changes

* receive page + address selection page

* receive page + address selection page

* add font

* add svg

* minor ui fixes

* receive page fixes/improvements

* change receive page modal design

* further ui polish

* icons for infobox

* fix restricted import

* fix closing label modal

* - fix balance card appearance on address page
- fix search bar position
- fix mweb position in address type selector

* receive page fixes

* fix anonpay

* remove restricted import

* fixes

* fix receive on zec

* simplify chain id

* lint fix

* fix migration numbering

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* fix migration version

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* fix settings

* readability

* fix formatting

* move dismiss infobox to vm

* disable addr rotation on zcash

* remove _setEffects

* translations

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* New UI Sync bar (#2831)

* syncbar

* small styling fixes

* modify duration

* lint.sh

* syncHeavy wallet types

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* update translations, add pull-down refresh, separate top bar components to files

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* alphabetize

* New design balance card icons (#2824)

* Add the other crypto card icons

* Update Zano, add Zcash and Arbitrum

* Update default card style and available gradients

* Fix broken preview

* Fix animation

* Color combinations fix

* Minor updates

* Add decred icon

* if(!mounted) return;

* New design minor fixes (#2842)

* More minor fixes

* Update modal sheet behaviour

* Update missing currency image links

* List row and navigation bar tweaks

* Minor navbar fix

* New UI Swap page (#2829)

* new ui send page (wip)

* coin control page improvements

* fee settings, new send confirmation modal

* minor aesthetic fixes

* load network name without cw_bitcoin import

* recipient dot cleanup

* minor logic fixes

* ui polish

* - add aliaspay support
- fix wording for max button
- fix back button action in modals

* Sizing changes

* dismiss transaction on modal close

* sizing fix

* - fix remove recipient icon
- properly round amounts
- fix coin control page spacing

* - fix remove recipient icon
- properly round amounts
- fix coin control page spacing

* - fix button sizing
- add description for fees page

* swap page

* fixes

* further fixes

* merge

* png -> svg

* minor condition changes

* lint.sh

* fix swapsxyz isCentralized

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* translations, readability improvements, remove debug rows

* cleanup

* alphabetize strings

* small bugfixes

---------

Co-authored-by: tuxsudo <tuxsudo@tux.pizza>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* fix new ui coin control page layout (#2843)

* New design lightning flows (#2821)

* new ui send page (wip)

* coin control page improvements

* fee settings, new send confirmation modal

* minor aesthetic fixes

* load network name without cw_bitcoin import

* recipient dot cleanup

* minor logic fixes

* ui polish

* - add aliaspay support
- fix wording for max button
- fix back button action in modals

* Sizing changes

* dismiss transaction on modal close

* sizing fix

* - fix remove recipient icon
- properly round amounts
- fix coin control page spacing

* - fix remove recipient icon
- properly round amounts
- fix coin control page spacing

* - fix button sizing
- add description for fees page

* lightning flow wip

* lightning flow wip #2

* lightning withdraw flow

* lightning deposit flow (wip, currently works but really janky)

* send from external + bugfixes

* merge fixes

* merge fixes

* fixes

* Update lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart

---------

Co-authored-by: tuxsudo <tuxsudo@tux.pizza>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* fix covered create wallet button

* fix: close modal after transaction commitment in Send ViewModel (#2858)

* New design tweaks (#2847)

* Add more haptic feedback

* Fix background gradients and modify SafeArea in home_page.dart

* Fix history padding, fix divider color, disable Charts, update base_page.dart appbar style, fix nav bar border + height on iOS

* Lightning Username setup flow for new UI (#2845)

* lightning username edit/create flow

* fix button color

* fix layout

* fixes

* remove character limitation for internal builds

* remove dependency on cw_bitcoin

* fix username requirements

* fixes

* condition fix

* Update lib/new-ui/pages/lightning_username_page.dart

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>

* keyboard hide overlay for iphone

* New design UI tweaks (#2870)

* Fix nav height on Android

* Fix blue gradient

* Update transactions view and minor UI tweaks

* CW1011-Allow-users-to-view-bitcoin-amounts-in-sats (#2678)

* feat: introduce `formatFixed` integration and sats display preference

* feat: enable display of Bitcoin amounts in satoshis and refactor amount formatting logic

* refactor: consolidate fee formatting logic and add support for MWEB availability check

* feat: add support for satoshi-based amount display and enhance fiat conversion logic

* feat: add support for satoshi-based amount parsing, formatting, and display preferences across buy/sell flow and UI adjustments

* feat: implement `AmountParsingProxy` with unit tests for parsing and formatting crypto amounts in various display modes

* feat: replace `preferBalanceInSats` with `displayAmountsInSatoshi` and introduce `AmountParsingProxy` for unified crypto amount handling across the app

* refactor: streamline amount formatting logic and enhance unspent coins handling with `AmountParsingProxy` integration for unified crypto processing

* refactor: replace direct bitcoin amount formatting with `AmountParsingProxy` and remove debug print in `exchange_view_model`

* refactor: replace `getIt<AmountParsingProxy>` with `_appStore.amountParsingProxy`, remove redundant dependencies across view models and adjust related imports

* refactor: remove redundant `AmountParsingProxy` registration in DI setup to simplify dependency management [skip ci]

* feat: add getCryptoSymbol to simplify the Symbol selection between Sats and Bitcoin

* fix: merge conflict

* Revert "fix: merge conflict"

This reverts commit 6debdaa0466747d6fcc85d6bd0be115e6fb4d856.

* fix import [skip ci]

* feat: remove number formatter from exchangeVM

* refactor: replace `SettingsStore` with `AppStore` across view models, integrate `AmountParsingProxy` for amount formatting, and clean up redundant imports

* refactor: remove debug print statements from `getCryptoOutputAmount` in `AmountParsingProxy`

* feat: enhance fee and deposit amount formatting with crypto symbols and improved parsing

* feat: integrate `AmountParsingProxy` for improved Bitcoin amount parsing in `PaymentURI` and fiat-to-crypto conversion

* feat: append crypto symbols to Bitcoin transaction amounts in dashboard view

* feat: improve crypto amount parsing in SendCard widget and Output view model using `AmountParsingProxy`

* feat: conditionally show Bitcoin amount display setting for Bitcoin wallets and refactor `DisplaySettingsViewModel` to use `AppStore`

* refactor: clean up redundant imports in `bitcoin_wallet.dart` and `electrum_wallet.dart`

* feat: append crypto symbols to fee values in send and cake pay card widgets for improved clarity

* refactor: replace `currencyTitle` with `currency` across widgets and integrate `AmountParsingProxy` for improved crypto amount formatting and parsing

* refactor: improve validation logic and amount parsing in SendCard and Output widgets

* refactor: standardize amount parsing and formatting logic with `AmountParsingProxy` across ViewModels and widgets

* refactor: `AppStore` remove debug prints

* refactor: rename amount parsing/formatting methods for better clarity in tests [skip ci]

* fix: AmountParsingProxy and optimize `cryptoAmount` handling in BuySellViewModel

* fix: adjust balance calculation for Monero and Wownero to exclude unlocked amount from full balance

* fix: correct `_appStore` usage and format adjustments in `SendViewModel`

* feat: enhance satoshi on new-ui

* feat: add computed property for `amountParsingProxy` in AppStore

* feat: localize Bitcoin amount display option and update asset history UI

* feat: improve exchange amount processing and refactor fiat conversion values

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>

* New design unconfirmed balance widget (#2861)

* unconfirmed balance display widget for new ui

* unconfirmed balance display widget for new ui

* merge

* alphabetize translations

* New UI fixes (#2860)

* fix asset top bar

* improve wordmark in large qr mode on receive

* receive option fixes

* exchange -> swap

* send page fixes

* update pubspec base

* go to home on wallet change

* nicer card customizer

* don't show account name for single-account wallets

* special card design for lightning

* fiat amount bar improvement

* update translations

* add sending indicator

* fix non-number input to receive amount

* de-jank fiat input

* nicer balance card animation when switching btc-ln

* nicer balance card animation when switching btc-ln

* fix backup password input

* display time+date in history

* fix error on buy

* fix incorrect wallet showing in wallets tab

* fix wallet list layout

* only show coin control option on hasCoinControl

* better error on send page

* fiat value in coin control

* add zano to isSyncHeeavy

* fix keys in settings

* fix assets condition

* fix import

* remove BasePage dependency in RescanPage

* add missing settings

* show send/receive in swap confirm sheet

* fix receive background on ios

* fix assets spacing

* fix trade tile sizing

* fix padding

* fix icon in wallet select modal

* change text on swap initial modal

* spacing

* l2 send external modal styling

* fix null on swap confirm sheet

* change infobox text

* add chain icon to asset widgets

* add chain icon to asset widgets

* disable swap page reset

* fix wallet list page layout

* revert merge conflict issue [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Account Customizer for new UI Monero (#2859)

* new ui monero account management (wip)

* account reordering wip

* account reordering wip

* account reordering (works)

* cards view compact mode

* format

* translation

* name generation

* fix for non-xmr wallets

* }

* allow reset order, fallback on improperly saved order

* translation

* fix for pre-reorder wallet loading

* fix loading

* - save cards on modal slide-down
- always load active account as first

* fix card customizer close anim

* fixes

* fix condition

* merge

* fix migration versions

* use updated flutter rust bridge in lightning and payjoin

* mvp fixes

* new ui mweb mask/unmask

* fallback for balance card amount format

* New UI tweaks (#2872)

* Update lightning transaction history view

* Add BNB card design

* Color, padding,  and radius fixes

* Fix wallet list bottom section & gradient

*  Update Apps page

* reformat

* nicer sat picker

* fix keyboard

* chain badges

* enhance styling for card customizer

* mweb settings icon

* mweb chain badge

* update wording in settings

* bnb chain icon

* always round send amount

* always round all amount

* token placeholder

* Fix ModernButton colors (#2888)

* Fix ModernButton colors

* Minor

* guard for unconfirmed balance widget

* fix error on wallet switch

* New design mvp fixes (#2890)

* mvp fixes

* fallback for balance card amount format

* reformat

* nicer sat picker

* fix keyboard

* chain badges

* enhance styling for card customizer

* bnb chain icon

* always round send amount

* always round all amount

* guard for unconfirmed balance widget

* fix error on wallet switch

* Update lib/new-ui/pages/account_customizer.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* fixes

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Install Protoc in reusable build workflow

Added step to install Protoc before building.

* Fix protobuf-compiler installation step

Update package list before installing protobuf-compiler.

* Modify Protoc installation command to handle update errors

Change the install command for Protoc to avoid failure on update.

* Streamline Lightning wallet balance management and crypto handling (#2893)

* refactor: streamline crypto amount handling and improve Lightning balance management

- Removed redundancy in balance calculations for Lightning wallets.
- Centralized crypto symbol creation with `selectedCryptoCurrencySymbol`.
- Enhanced crypto amount handling and formatting with `AmountParsingProxy`.
- Simplified logic for displaying Lightning balances across UI components.

* refactor: remove sats display logic and unused formatting from BalanceCard widget

* chore: update breez-sdk-spark-flutter to latest commit

* feat: enhance crypto amount formatting with localized separators and max decimals

* fixes

* fix: handle null balance records in Lightning wallet cards

* New design add validators (#2894)

* add validation, fix node form

* Add validator to send amount input field

Added a validator to the amount input field.

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Disable 'addresses' button on receive screen for BTC LN (#2896)

* feat: Disable addresses button on receive screen for BTC LN

* chore: remove debug print in uri computation

* feat: introduce swap action toggle and UI updates

* feat: conditionally display Zcash card setting for Zcash wallets in Display Settings

* Fix leading button color (#2898)

* fix amount format

* fix loadCards

* pre-beta fixes

* cyjan: fix: order being null for certain index

* pre-beta fixes v2 (#2905)

* minor context check [skip ci]

* persist fee priority for zcash (#2907)

* Add support for Zcash address detection and amount logic refactor (#2906)

* refactor: consolidate crypto amount handling with `displayAmount` and `cryptoCurrencySymbol`, improve fiat conversion logic, and optimize token currency management

* refactor: simplify `displayAmount` logic and remove unused `_rawAmount` property

* feat: add support for Zcash transparent address detection and simplify amount conversion logic

* Improve Lightning invoice handling and crypto formatting (#2908)

* fix: update Lightning address regex to support additional formats and prefixes

* fix: conditionally render advanced settings dropdown based on fees or coin control availability

* feat: enhance address detection, Lightning invoice handling, and crypto formatting

* feat: improve Lightning invoice parsing and crypto amount formatting on send page

* chore: remove unnecessary exception printing in `getBolt11Amount` function

* Fix lightning swap

* import fix (hide Mac;)

* parse -> tryParse

* Transaction Commited -> Transaction Sent!

* feat: enable clipboard paste for deposit and receive addresses in swap page (#2909)

* fix: resolving LNURLS (#2911)

* new UI pre-beta fixes (#2910)

* fixes

* parse -> tryParse

* change sat display settings style

* minor fix until hive is removed [skip ci]

* Remove redundant logic and improve send page features (#2913)

* fix: update crypto amount parsing logic in send confirmation sheet

* feat: Added checkbox to toggle Litecoin MWEB coins on the send page.
fix: LNUrlPayRecord

* fix: remove redundant `.withLocalSeperator()` from fiat balance formatting logic

* fix: update Bitcoin amount display titles to align with consistent formatting standards

* force boolean to update (#2916)

* force boolean to update

* fix bnb balance

* fixes #4 (#2917)

* only display sats for btc wallets in card customizer

* fix: resolve async handling of Lightning transaction history in `fetchTransactions` method (#2919)

* New design pre beta fixes 5 (#2918)

* improve l2 deposit/withdraw flow layouts

* add copied indicator

* SafeArea

* fix addr rotation

* fix styling of lightning switcher

* no raster graphics in new ui please

* optimization for assets/history section

* remove unused bloc provider

* optimize dashboard + fix duplicated name

* disable lightning mode on wallet change

* remove broken animation

* safearea around nodes page

* fix rescan page layout

* fix exception on really fast switching

* remove broken blur

* card customizer autosave

* remove debug print

* add better fallbacks for currency titles

* revert due to broken animation

* fix send page crash

* fix chain title for arb

* fix cupertino date picker

* Update lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* feat: add focus management and external address validation to send page forms

- Introduced `focusNode` to `NewSendAddressInput` for better input handling.
- Enhanced address validation by synchronizing with parsed external addresses.
- Updated logic for non-MWEB unspent coin type in send actions.
- Suppressed unnecessary exception logs in Bitcoin wallet transactions.

* New design swap page fixes (#2923)

* swap page fixes

* add scroll controller

* comment

* tweak refresh pulldown (#2924)

* add copy button to l2 send external modal (#2925)

* account customizer padding

* feat: lightning transaction fetching and Lightning wallet integration (#2927)

- Allow nullable `height` in `ElectrumTransactionInfo`.
- Add `fromDate` support for filtered Lightning transaction history retrieval.
- Simplify Lightning address handling logic in wallet addresses.
- Optimize Lightning wallet transaction syncing based on the last Lightning transaction date.
- Clean up imports in `electrum_transaction_history.dart`.

* New balance handeling (#2931)

* feat: lightning transaction fetching and Lightning wallet integration

- Allow nullable `height` in `ElectrumTransactionInfo`.
- Add `fromDate` support for filtered Lightning transaction history retrieval.
- Simplify Lightning address handling logic in wallet addresses.
- Optimize Lightning wallet transaction syncing based on the last Lightning transaction date.
- Clean up imports in `electrum_transaction_history.dart`.

* feat: add support for Lightning balance in wallet snapshot and initialization

* Disable Lightning switcher for hardware wallets (#2932)

* fix: disable Lightning switcher for hardware wallets

* fix: update wallet type check to use `isSoftwareWallet` for Lightning support

* New design pre beta fixes 6 (#2929)

* fix swaps from ln

* prevent crash if ln balance isn't loaded yet

* fix currency icons disappearing on swap confirm sheet

* fix swap fiat validation

* display ln at top on swap currency selector in btc wallets

* update icon for card options

* display default fiat and crypto at top of currency picker

* fix fiat amount bar formatting

* add modal scroll controller to provider logs

* revert premature optimization (sizing issue)

* New design pre beta fixes 7 (#2935)

* fix swaps from ln

* prevent crash if ln balance isn't loaded yet

* fix currency icons disappearing on swap confirm sheet

* fix swap fiat validation

* display ln at top on swap currency selector in btc wallets

* update icon for card options

* display default fiat and crypto at top of currency picker

* fix fiat amount bar formatting

* add modal scroll controller to provider logs

* revert premature optimization (sizing issue)

* copyable trade id

* disable swaptrade on ln

* set isFixedRate properly

* update color

* display trocador provider on swap confirmation sheet

* add "exchange rate is fixed" text

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* updated l2 modal (#2934)

* display names for send addr input (#2936)

* fix: MWEB toggle logic on send page (#2938)

* feat: add retry logic to `getAddress` in Lightning wallet (#2939)

* Enhance crypto address handling and add Lightning invoice support (#2937)

* feat: enhance crypto address handling and add Lightning invoice support

- Added `_overrideFromCryptoCurrency` and `_overrideToCryptoCurrency` for Zcash unified and shielded addresses.
- Improved Lightning invoice generation with `getLightningInvoice` for Bitcoin wallets.
- Updated error handling in exchange provider to accommodate API changes.
- Introduced Lightning invoice fallback for deposit and receive addresses in ExchangeViewModel.

* feat: Added `_overrideFromCryptoCurrency` and `_overrideToCryptoCurrency` for Zcash unified and shielded addresses.

* fix pay anything on paste button (#2940)

* Prevent multiple paste triggering any pay flow

* fixup fiat input (#2941)

* New design small fixes (#2920)

* Fix optical symmetry for swipe to send text

* Update nav bar icons

* Top bar and card tweaks

* Update paddings

---------

Co-authored-by: malik1004x <malikowskirobert@gmail.com>

* fix erc20 balance display (#2944)

* fix erc20 balance display

* route through cw_evm

* New design beta fixes (#2943)

* fix asset tile layout

* fix card customizer keyboard jumping

* fix cardsview exception

* fix swap modal height

* remove syncbar percentage

* add copy icon to trade id

* fix trade confirmation appearing twice

* remove useless mweb setting

* add tag to contact currency selector text

* prettify picker for ln

* fix receive crash

* always route to NewSendPage

* handle ln amount on paste

* handle ln amount on payment request creation

* prettify ln errors

* readd "blocks" word

* register deeplinks

* Enhance bitcoin error message [skip ci]

* fix support chat page (#2946)

* new-design-fix-confirm-sheet-parsing-error (#2950)

* feat: use bigint instead of doubles

* feat: use bigint instead of doubles

* feat: use bigint instead of doubles

* fix: also show sats in confirm sheet

* fix: also show sats in confirm sheet

* feat: localize amounts and cap amount in tx history (#2951)

* fix: linux CI

* feat: localize token balances (#2954)

* fix: send amount hww (#2955)

* chore: use latest breez sdk (#2953)

* fix addAddressWord

* fixes (#2957)

* receive fixes (#2959)

* fix: stabilize android build by increasing jvm heap size (#2958)

* fix: lighting by having the new SDK Save into a new location (#2961)

* New design rc fixes (#2960)

* additional safeguards for isSyncHeavy check

* fix account name getting reset

* update base icon

* fix units in swap

* readd memos to zec

* New design minor UI (#2956)

* Wrap text for list item widgets

* Update lightning address and QR icons

* Update near intents icon

* Fix share issue

* Fix (#2963)

* fix tx amount formatting

* rename currency

* change wording for zec memo

* fix base icon

* fix app icon script [skip ci]

* New design rc fixes 2 (#2965)

* fix account name changes

* fix spacing on addresses page

* add payjoin copy modal

* improve memo ui

* potential fix for ci

* Set default address for zcash and bitcoin for buy flow (#2979)

* new monero.com icons (#2980)

* parse and validate deposit amount from api (#2967)

* generic fixes

* minor context fix [skip ci]

* Generic fixes (#2982)

- solana send all bug
- pasting SOL address triggering payanything flow
- add more address types for payanything detection

* rescan fix

* about page (#2974)

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* update settings icons/wording (#2973)

* Changelog modal (New UI) (#2971)

* add new changelog modal

* typo

* print -> printV

* Update lib/new-ui/widgets/changelog_modal.dart [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* fix node form (#2970)

* new-design-add-breez-logging (#2984)

* feat: add Lightning log export functionality for Bitcoin wallets

* feat: add multilingual support for Lightning log export strings

* feat: add Lightning support toggle in settings and translations for multiple languages

* refactor: simplify `setUseLightning` by removing redundant store update

* Update cw_bitcoin/lib/electrum_wallet.dart [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* minor fixes [skip ci]

* minor context fixe [skip ci]

* fix: make walletinfoid unique to prevent duplicates (#2969)

* fix: walletconnect not working in new design builds (#2993)

* new-design-fix-mweb-coin-enabling (#2995)

* feat: add unspent coin type handling to asset details modal send button

* fix: improve German translations for wallet, seed, and Lightning-related strings

* fix: update Dutch translations to correct terminology and formatting #2992 (#2997)

* fix: drop gn (#3000)

* vulnerable seeds popup on new ui (#3004)

* update mac os monero.com icons (#3003)

* Add Payjoin log export and improve logging behavior (#2999)

* fix: suppress exception logging in `getAddress` method of Lightning wallet

- Added a try-catch block to avoid unnecessary exception logs during Lightning address retries.

* fix: update fallback message for missing Lightning address detection

* feat: add Payjoin log export and logging enhancements

* fix: localize "Export Payjoin Logs" string in settings page

* fix: litecoin mweb balance being included in the second asset (#2998)

* Minor fixes (#2994)

* Minor fixes

* Update lib/new-ui/new_dashboard.dart [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* New design post release fixes (#3002)

* fix: only throw on deserialize if kDebugMode is true (#2976)

This closes #2972, in debug mode we will still catch these issues but if
somehow that happens on production it will fallback to safe default

* fix: update docs and dockerfile (#2975)

* fix: missing steps for linux build [skip ci] (#2985)

* move balance to bigint

* move balance to bigint

* fix electrum issue

* fix unconfirmed balance

* fixes

* Fix minor issue in Spanish translation (#2991)

* Fix minor issue in French translation (#2987)

* Fix minor issues in German translation (#2986)

* update constructor calls in configure.dart

* fixes

* remove expnded

* V6.0.1 Early Release Candidate

---------

Co-authored-by: cyan <cyjan@mrcyjanek.net>
Co-authored-by: Edwin den Boer <woordenboer@planet.nl>
Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>

* Swaps xyz swap enhancements (#2615)

* refactor token cache and limit handling

* Improve SwapsXYZ external send logic

* Improve SwapsXYZ fixed-rate handling

* disable fixedRate for SwapsXyz

* SwapsXYZ contract call

* Add track url for swaps xyz

* add arbitrum support

* add check for unsupported source tokens

* enable SwapsXyzExchangeProvider in provider list

* refactor SwapsXYZ provider and trade flow handling

* Update exchange_trade_view_model.dart

* validate evm call-data

* Revert "validate evm call-data"

* fix msg value for ERC-20 transfers

* fix flag for swapsXYZ send check

* Update send_view_model.dart

* handle Swaps.xyz method selectors & approvals

* fix evm approvals

* Improve EVM token approval & receipt handling

* merge split token transfers in evm tx

* fix token allowance check for approvals

* add sourceToken validation to EVM tx creation

* prioritizing incoming transactions over fetched outgoing ones

* compute net flow when merging transactions

* add truncateTrailingText to list items and widgets

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Co-authored-by: Serhii-Borodenko <17529954+Serhii-Borodenko@users.noreply.github.com>

* fix wownero balance (#3007)

* fix keyboard (#3006)

* fix android build

* check if stream is closed [skip ci]

* [skip ci] lint

* fix: populate amount field (#3009)

* fix address rotation exception (#3011)

* copy uri with amount from recieve page (#3010)

* add "transaction sent!" to swap confirmation sheet (#3012)

* fix wallet change on token swaps (#3013)

* Fix fee estimation and recipient logic in Lightning Network (#3014)

* fix: update `hasMultiRecipient` logic to include non-Lightning coin type check

* fix: Lightning Network fee estimation and parsing logic

* fix: adjust `sumByBigInt` logic to handle `sendAll` flag in send page

* fix: move sqlite migrations to sqlite.dart (#3008)

* fix: move sqlite migrations to sqlite.dart
fix: backup restore not initializing SQL properly

* Update cw_core/lib/db/sqlite.dart

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* feat: update currency symbol handling and extend Electrum transaction details (#3016)

* Use isCentralized for truncation trade id [skip ci]

* fix: Incorrect token amount for tokens and filter out new spam transactions after swaps (#3015)

* New design release swap fixes (#3018)

* fix deserialize ln

* fix get wallet type for bnb tokens

* show extraId for swaps

* chore: translate using gpt-5.2 (#3021)

* add NEAR send-all validation

* tiny ui fixes (#3023)

* use double parsing for amount comparison[skip ci]

* adjust lightning address error message

* feat: add BTC LN support to available methods and payment data handling

* feat: add cached Lightning address handling for improved performance (#3025)

- Introduced `cachedAddress` field to store and reuse the last fetched Lightning address.
- Updated `getLightningAddress` method to fallback on `cachedAddress` if no new address is fetched.
- Extended wallet constructors and snapshots to include `cachedLightningAddress` for persistence.

* Revert transaction history color

* Minor

* Apply suggestion from @malik1004x

* new-design-post-release-fixes-3 (#3028)

* fix unnecessary account switching

* fix exception on bad payment amount

* fallback for currencies on external swap modal

* increase balance card touch target

* don't use lightning addresses for exchange

* increase max length for ln encoding

* don't show infobox on ln receive

* fix showing old tx details (null tx on createTransaction)

* remove unused import

* refactor: relocate `_logStream` declaration in `LightningWallet` for better code organization (#3029)

* monero build fix
update build numbers
fix best rate null [skip ci]

* hide swap on mweb asset

* minor context fix [skip ci]

* Fix Silent payment case

* new-ui: fix changelog typo (#3036)

Fix typo in changelog for account management.

* Modify date format to Month DD YYYY

* feat: docker based ci

* Fix 2FA continue button location (#3043)

* fix: Desktop wallet bugs (#2968)

* fix: Desktop wallet bugs
- repeated wallets post migration
- newly created wallets not showing up
- extra large components on desktop, needed maxwidth

* chore: Remove duplicate logic

* chore: remove formatting

* refactor: streamline updateList method and handle multiple calls with a future based mutex

* refactor: enhance swipe to confirm with dynamic width handling and improved drag threshold logic

* Improve error handling and duplicate invoice handling in Lightning wallet (#3047)

* fix: handle duplicate Lightning invoice payments gracefully

* fix: improved error handling in Lightning wallet

* General improvements (#3030)

* General Improvements
- Make borderRadius for CTAs uniform 18px
- Identify hardware wallets in title of Seeds/Keys page
- Make entire wallet tile clickable in wallet list

* chore: Simplify check

* Update lib/view_model/wallet_keys_view_model.dart [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* homescreen improvements (#3034)

* Homescreen improvements
- Increase touch area for card editing icon
- Tapping any card in the background should move it ino the foreground without closing the account tab
- make top left chain icon primary color with full opacity when syncing and 20% when done
- add tor indicator to top bar when enabled

* fix: remove unneeded color filter

* refactor: Adjust structure for tor and sync indicators

* minor fixes (#3045)

* Enhance LNURL payment integration (#3041)

* feat: enhance LNURL support and Lightning payment handling

- Improve LNURL decoding and encoding with expanded prefix handling.
- Add support for parsing and validating LNURL payment invoices.
- Introduce logic for fetching LNURL payment request amounts.
- Update payment flow to integrate LNURL and Lightning-specific conditions.
- Refactor BOLT11 prefix validation and amount extraction logic.

* fix: simplify LNURL invoice validation by removing redundant BOLT11 check

---------

Signed-off-by: black5box <black5box@outlook.com>
Co-authored-by: Serhii <borodenko.sv@gmail.com>
Co-authored-by: malik1004x <malikowskirobert@gmail.com>
Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
Co-authored-by: tuxsudo <tuxsudo@tux.pizza>
Co-authored-by: black5box <black5box@outlook.com>
Co-authored-by: cyan <cyjan@mrcyjanek.net>
Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com>
Co-authored-by: Konstantin Ullrich <konstantin@cakewallet.com>
Co-authored-by: Edwin den Boer <woordenboer@planet.nl>
Co-authored-by: Serhii <17529954+serhii-bor@users.noreply.github.com>
Co-authored-by: Serhii-Borodenko <17529954+Serhii-Borodenko@users.noreply.github.com>
Co-authored-by: rottenwheel <92872541+rottenwheel@users.noreply.github.com>
This commit is contained in:
Omar Hatem
2026-03-11 04:26:57 +02:00
committed by GitHub
co-authored by Serhii malik1004x Konstantin Ullrich tuxsudo black5box cyan David Adegoke Konstantin Ullrich Edwin den Boer Serhii Serhii-Borodenko rottenwheel
parent 18c36acc14
commit 084fd6bb1e
775 changed files with 60901 additions and 29638 deletions
+108 -34
View File
@@ -153,16 +153,17 @@ class CWBitcoin extends Bitcoin {
return BitcoinTransactionCredentials(
outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo,
extra: out.extra))
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo.isNotEmpty ? out.memo : null,
extra: out.extra,
))
.toList(),
priority: priority as BitcoinTransactionPriority,
feeRate: bitcoinFeeRate,
@@ -264,6 +265,7 @@ class CWBitcoin extends Bitcoin {
return element.bitcoinAddressRecord.type == SegwitAddresType.mweb;
case UnspentCoinType.nonMweb:
return element.bitcoinAddressRecord.type != SegwitAddresType.mweb;
case UnspentCoinType.lightning:
case UnspentCoinType.any:
return true;
}
@@ -319,30 +321,6 @@ class CWBitcoin extends Bitcoin {
return bitcoinWallet.walletAddresses.addressPageType == SilentPaymentsAddresType.p2sp;
}
@override
List<ReceivePageOption> getBitcoinReceivePageOptions(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
final keys = bitcoinWallet.keys;
if (keys.privateKey.isEmpty) {
return BitcoinReceivePageOption.allViewOnly;
}
return BitcoinReceivePageOption.all;
}
@override
List<ReceivePageOption> getLitecoinReceivePageOptions(Object wallet) {
final litecoinWallet = wallet as ElectrumWallet;
if (Platform.isLinux ||
Platform.isMacOS ||
Platform.isWindows ||
litecoinWallet.isHardwareWallet) {
return BitcoinReceivePageOption.allLitecoin
.where((element) => element != BitcoinReceivePageOption.mweb)
.toList();
}
return BitcoinReceivePageOption.allLitecoin;
}
@override
BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) {
switch (option) {
@@ -362,6 +340,13 @@ class CWBitcoin extends Bitcoin {
}
}
@override
BitcoinReceivePageOption getBitcoinLightningReceivePageOption() => BitcoinReceivePageOption.lightning;
@override
BitcoinReceivePageOption getBitcoinSegwitPageOption() => BitcoinReceivePageOption.p2wpkh;
@override
BitcoinReceivePageOption getLitecoinMwebReceivePageOption() => BitcoinReceivePageOption.mweb;
@override
Future<List<DerivationType>> compareDerivationMethods(
{required String mnemonic, required Node node}) async {
@@ -703,6 +688,8 @@ class CWBitcoin extends Bitcoin {
}
List<Output> updateOutputs(PendingTransaction pendingTransaction, List<Output> outputs) {
if (pendingTransaction is PendingLightningTransaction) return outputs;
final pendingTx = pendingTransaction as PendingBitcoinTransaction;
if (!pendingTx.hasSilentPayment) {
@@ -780,6 +767,15 @@ class CWBitcoin extends Bitcoin {
}
}
Future<String?> getUnusedSpakDepositAddress(Object wallet) async {
try {
final bitcoinWallet = wallet as BitcoinWallet;
return wallet.lightningWallet?.getDepositAddress();
} catch (_) {
return null;
}
}
@override
Future<void> commitPsbtUR(Object wallet, List<String> urCodes) {
if (wallet is LitecoinWallet) return wallet.commitPsbtUR(urCodes);
@@ -804,6 +800,22 @@ class CWBitcoin extends Bitcoin {
}
}
@override
bool useLightning(Object wallet) {
final _wallet = wallet as ElectrumWallet;
if (_wallet is BitcoinWallet) return _wallet.useLightning;
return false;
}
@override
void updateUseLightning(Object wallet, bool value) {
final _wallet = wallet as ElectrumWallet;
if (_wallet is BitcoinWallet) {
_wallet.useLightning = value;
}
}
@override
void resumePayjoinSessions(Object wallet) {
final _wallet = wallet as ElectrumWallet;
@@ -839,4 +851,66 @@ class CWBitcoin extends Bitcoin {
return addresses;
}
@override
String getNetworkName(Object wallet) {
return (wallet as ElectrumWallet).network.value;
}
@override
Future<void> setLightningUsername(Object wallet, String username) async {
final electrumWallet = wallet as ElectrumWallet;
await electrumWallet.walletAddresses.setLightningAddress(wallet.name, newAddress: username);
}
@override
Future<String?> getLightningUsername(Object wallet) async {
final electrumWallet = wallet as ElectrumWallet;
if (electrumWallet.walletAddresses.lightningWallet == null) {
printV("lightning wallet is null");
return null;
}
return (await electrumWallet.walletAddresses.lightningWallet!.getAddress())
?.replaceFirst("@cake.cash", "");
}
@override
Future<String?> getLightningInvoice(Object wallet, BigInt amount) async {
final electrumWallet = wallet as ElectrumWallet;
if (electrumWallet is BitcoinWallet && electrumWallet.lightningWallet != null) {
return electrumWallet.lightningWallet!.getBolt11Invoice(amount, "Send to CakeWallet");
}
return null;
}
@override
String? getBreezSdkError(Object exception) {
if (exception is SdkError_SparkError) {
return (exception as SdkError_SparkError).field0.toString();
}
if (exception is SdkError_InvalidUuid) {
return (exception as SdkError_InvalidUuid).field0.toString();
}
if (exception is SdkError_InvalidInput) {
return (exception as SdkError_InvalidInput).field0.toString();
}
if (exception is SdkError_NetworkError) {
return (exception as SdkError_NetworkError).field0.toString();
}
if (exception is SdkError_StorageError) {
return (exception as SdkError_StorageError).field0.toString();
}
if (exception is SdkError_ChainServiceError) {
return (exception as SdkError_ChainServiceError).field0.toString();
}
if (exception is SdkError_LnurlError) {
return (exception as SdkError_LnurlError).field0.toString();
}
if (exception is SdkError_Generic) {
return (exception as SdkError_Generic).field0.toString();
}
return null;
}
}
-1
View File
@@ -21,7 +21,6 @@ import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
+1 -4
View File
@@ -8,7 +8,6 @@ import 'package:cake_wallet/buy/pairs_utils.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/core/theme_store.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
@@ -17,7 +16,7 @@ import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class OnRamperBuyProvider extends BuyProvider {
OnRamperBuyProvider(this._themeStore,
OnRamperBuyProvider(
{required WalletBase wallet, bool isTestEnvironment = false})
: super(wallet: wallet,
isTestEnvironment: isTestEnvironment,
@@ -39,8 +38,6 @@ class OnRamperBuyProvider extends BuyProvider {
static const List<FiatCurrency> _notSupportedFiat = [];
static Map<String, dynamic> _onrampMetadata = {};
final ThemeStore _themeStore;
String? recommendedPaymentType;
String get _apiKey => secrets.onramperApiKey;
-2
View File
@@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:cake_wallet/core/selectable_option.dart';
enum PaymentType {
@@ -3,7 +3,7 @@ import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_tile.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_account_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@@ -1,12 +1,11 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart';
@@ -6,7 +6,7 @@ import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart';
@@ -619,11 +619,16 @@ class CakePayBuyCardPage extends BasePage {
titleIconPath: _sendViewModel.selectedCryptoCurrency.iconPath,
currency: _sendViewModel.selectedCryptoCurrency,
amount: S.of(bottomSheetContext).send_amount,
amountValue: _sendViewModel.pendingTransaction!.amountFormatted,
amountValue: _sendViewModel.amountParsingProxy.getDisplayCryptoAmount(
_sendViewModel.pendingTransaction!.amountFormatted,
_sendViewModel.selectedCryptoCurrency),
quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted,
fee: S.of(bottomSheetContext).send_fee,
feeValue: _sendViewModel.pendingTransaction!.feeFormatted,
feeValue: "${_sendViewModel.amountParsingProxy.getDisplayCryptoAmount(
_sendViewModel.pendingTransaction!.feeFormattedValue,
_sendViewModel.selectedCryptoCurrency)} ${_sendViewModel.amountParsingProxy
.getCryptoSymbol(_sendViewModel.wallet.currency)}",
feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: displayingOutputs,
footerType: FooterType.slideActionButton,
@@ -640,6 +645,7 @@ class CakePayBuyCardPage extends BasePage {
},
change: _sendViewModel.pendingTransaction!.change,
isOpenCryptoPay: _sendViewModel.ocpRequest != null,
amountParsingProxy: _sendViewModel.amountParsingProxy,
);
},
);
+1 -2
View File
@@ -65,8 +65,7 @@ class CakePayOrder {
CakePayPaymentMethod.LTC => order.paymentData.ltc,
CakePayPaymentMethod.LTC_MWEB => order.paymentData.ltc_mweb,
CakePayPaymentMethod.ZEC => order.paymentData.zec,
// TODO: Handle this case.
CakePayPaymentMethod.BTC_LN => throw UnimplementedError(),
CakePayPaymentMethod.BTC_LN => order.paymentData.btc_ln,
};
if (data == null) return null;
+29 -16
View File
@@ -12,21 +12,29 @@ const AFTER_REGEX = '(\$|\\s)';
class AddressValidator extends TextValidator {
AddressValidator({required CryptoCurrency type, bool isTestnet = false})
: super(
errorMessage: S.current.error_text_address,
useAdditionalValidation: type == CryptoCurrency.btc || type == CryptoCurrency.ltc
? (String txt) => BitcoinAddressUtils.validateAddress(
address: txt,
network: type == CryptoCurrency.btc
? isTestnet
? BitcoinNetwork.testnet
: BitcoinNetwork.mainnet
: LitecoinNetwork.mainnet,
)
: type == CryptoCurrency.zano
? zano?.validateAddress
: null,
pattern: getPattern(type, isTestnet: isTestnet),
length: getLength(type));
errorMessage: S.current.error_text_address,
useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type)
? (String txt) {
final RegExp lightningInvoiceRegex = RegExp(
r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl)[a-z0-9]+$',
caseSensitive: false);
if (lightningInvoiceRegex.hasMatch(txt)) return true;
return BitcoinAddressUtils.validateAddress(
address: txt,
network: type == CryptoCurrency.btc
? isTestnet
? BitcoinNetwork.testnet
: BitcoinNetwork.mainnet
: LitecoinNetwork.mainnet,
);
}
: type == CryptoCurrency.zano
? zano?.validateAddress
: null,
pattern: getPattern(type, isTestnet: isTestnet),
length: getLength(type),
);
static String getPattern(CryptoCurrency type, {bool isTestnet = false}) {
var pattern = "";
@@ -53,6 +61,7 @@ class AddressValidator extends TextValidator {
'|(bc1q[ac-hj-np-z02-9]{25,39})'
'|(bc1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))'
'|(bc1q[ac-hj-np-z02-9]{40,80})'
'|(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl)[a-z0-9]+'
'|(${silentPaymentAddressPatternMainnet})(\$|\s)';
}
case CryptoCurrency.ltc:
@@ -161,7 +170,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.pivx:
pattern = 'D([1-9a-km-zA-HJ-NP-Z]){33}';
case CryptoCurrency.btcln:
pattern = '(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)';
pattern = r'(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl|LNBC|LNTB|LNBS|LNBCRT|LNURL)[a-zA-Z0-9]+';
case CryptoCurrency.zano:
pattern = r'([1-9A-HJ-NP-Za-km-z]{90,200})|(@[\w\d.-]+)';
case CryptoCurrency.doge:
@@ -315,6 +324,10 @@ class AddressValidator extends TextValidator {
static String get silentPaymentAddressPatternTestnet => '(tsp|sprt)1[0-9a-zA-Z]{113}';
static String get mWebAddressPattern => MwebAddress.regex.pattern;
static const String bolt11InvoiceMatcher = r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$';
static const String bolt12OfferMatcher = r'^(lightning:)?(lno1)[a-z0-9]+$';
static const String lnurlMatcher = r'^(lightning:)?(lnurl)[a-z0-9]+$';
// NOTE: not needed to check for network here as it's a general address catcher, validation is separate
static String? getAddressFromStringPattern(CryptoCurrency type) {
String? pattern = null;
-37
View File
@@ -1,37 +0,0 @@
// abstract class Amount {
// Amount(this.value);
// int value;
// int minorDigits;
// String code;
// String formatted();
// }
// class MoneroAmount extends Amount {
// MoneroAmount(int value) : super(value) {
// minorDigits = 12;
// code = 'XMR';
// }
// // const moneroAmountLength = 12;
// // const moneroAmountDivider = 1000000000000;
// // final moneroAmountFormat = NumberFormat()
// // ..maximumFractionDigits = moneroAmountLength
// // ..minimumFractionDigits = 1;
// // String moneroAmountToString({int amount}) =>
// // moneroAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider));
// // double moneroAmountToDouble({int amount}) => cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider);
// // int moneroParseAmount({String amount}) => moneroAmountFormat.parse(amount).toInt();
// @override
// String formatted() {
// // TODO: implement formatted
// throw UnimplementedError();
// }
// }
+82
View File
@@ -0,0 +1,82 @@
import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart';
import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
class AmountParsingProxy {
final BitcoinAmountDisplayMode displayMode;
const AmountParsingProxy(this.displayMode);
/// [getCryptoInputAmount] turns the input [amount] into the canonical representation of [cryptoCurrency]
String getCanonicalCryptoAmount(String amount, CryptoCurrency cryptoCurrency) {
if (useSatoshi(cryptoCurrency) && amount.isNotEmpty) {
return cryptoCurrency.formatAmount(BigInt.tryParse(amount) ?? BigInt.zero);
}
return amount;
}
/// [getCryptoOutputAmount] turns the input [amount] into the preferred representation of [cryptoCurrency]
String getDisplayCryptoAmount(String amount, CryptoCurrency cryptoCurrency) {
if (useSatoshi(cryptoCurrency) && amount.isNotEmpty) {
return cryptoCurrency.parseAmount(amount.withMaxDecimals(cryptoCurrency.decimals)).toString();
}
return amount.withMaxDecimals(cryptoCurrency.decimals);
}
/// [getCryptoStringRepresentation] turns the input [amount] into the preferred representation of [cryptoCurrency]
String getDisplayCryptoString(int amount, CryptoCurrency cryptoCurrency) {
if (useSatoshi(cryptoCurrency)) {
return "$amount";
}
return cryptoCurrency.formatAmount(BigInt.from(amount));
}
String getDisplayCryptoStringFromBigInt(BigInt amount, CryptoCurrency cryptoCurrency) {
if (useSatoshi(cryptoCurrency)) {
return "$amount";
}
return cryptoCurrency.formatAmount(amount);
}
/// [getCryptoStringFromDouble] turns the input [amount] into the preferred representation of [cryptoCurrency] and
String getDisplayCryptoStringFromDouble(double amount, CryptoCurrency cryptoCurrency) {
if (useSatoshi(cryptoCurrency)) {
return "$amount";
}
return cryptoCurrency.formatAmount(BigInt.from(amount));
}
/// [parseCryptoString] turns the input [string] into a `BigInt` presentation of the [cryptoCurrency]
BigInt parseCryptoString(String amount, CryptoCurrency cryptoCurrency) {
if (useSatoshi(cryptoCurrency)) {
return BigInt.parse(amount);
}
return cryptoCurrency.parseAmount(amount);
}
/// [tryParseCryptoString] tries to turn the display representation [string] into a `BigInt` presentation of the [cryptoCurrency]
BigInt? tryParseCryptoString(String amount, CryptoCurrency cryptoCurrency) {
if (useSatoshi(cryptoCurrency)) {
return BigInt.tryParse(amount);
}
return cryptoCurrency.tryParseAmount(amount);
}
/// [getCryptoSymbol] returns the correct Symbol related to the presentation
String getCryptoSymbol(CryptoCurrency cryptoCurrency) =>
useSatoshi(cryptoCurrency) ? "sats" : cryptoCurrency.title.safeSubString(0, 8);
bool useSatoshi(CryptoCurrency cryptoCurrency) =>
([CryptoCurrency.btc, CryptoCurrency.btcln].contains(cryptoCurrency) &&
displayMode == BitcoinAmountDisplayMode.satoshi) ||
(CryptoCurrency.btcln == cryptoCurrency &&
displayMode == BitcoinAmountDisplayMode.satoshiForLightning);
}
+16
View File
@@ -1,3 +1,4 @@
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cw_core/crypto_currency.dart';
@@ -6,6 +7,7 @@ import 'package:cw_core/currency.dart';
class AmountValidator extends TextValidator {
AmountValidator({
required CryptoCurrency currency,
required AmountParsingProxy amountParsingProxy,
bool isAutovalidate = false,
String? minValue,
String? maxValue,
@@ -17,11 +19,15 @@ class AmountValidator extends TextValidator {
amountMinValidator = AmountMinValidator(
minValue: minValue,
isAutovalidate: isAutovalidate,
amountParsingProxy: amountParsingProxy,
cryptoCurrency: currency,
);
amountMaxValidator = AmountMaxValidator(
maxValue: maxValue,
isAutovalidate: isAutovalidate,
amountParsingProxy: amountParsingProxy,
cryptoCurrency: currency,
);
}
@@ -89,10 +95,14 @@ class AllAmountValidator extends TextValidator {
class AmountMinValidator extends Validator<String> {
final String? minValue;
final bool isAutovalidate;
final AmountParsingProxy amountParsingProxy;
final CryptoCurrency cryptoCurrency;
AmountMinValidator({
this.minValue,
required this.isAutovalidate,
required this.amountParsingProxy,
required this.cryptoCurrency,
}) : super(errorMessage: S.current.error_text_input_below_minimum_limit);
@override
@@ -105,6 +115,7 @@ class AmountMinValidator extends Validator<String> {
return true;
}
value = amountParsingProxy.getCanonicalCryptoAmount(value, cryptoCurrency);
final valueInDouble = parseToDouble(value);
final minInDouble = parseToDouble(minValue ?? '');
@@ -124,10 +135,14 @@ class AmountMinValidator extends Validator<String> {
class AmountMaxValidator extends Validator<String> {
final String? maxValue;
final bool isAutovalidate;
final AmountParsingProxy amountParsingProxy;
final CryptoCurrency cryptoCurrency;
AmountMaxValidator({
this.maxValue,
required this.isAutovalidate,
required this.amountParsingProxy,
required this.cryptoCurrency,
}) : super(errorMessage: S.current.error_text_input_above_maximum_limit);
@override
@@ -140,6 +155,7 @@ class AmountMaxValidator extends Validator<String> {
return true;
}
value = amountParsingProxy.getCanonicalCryptoAmount(value, cryptoCurrency);
final valueInDouble = parseToDouble(value);
final maxInDouble = parseToDouble(maxValue ?? '');
+1 -1
View File
@@ -260,7 +260,7 @@ Future<void> _handleDuressLogin(
// Wipe wallet-related database tables
try {
await db.transaction((txn) async {
await db!.transaction((txn) async {
await txn.delete(WalletInfoAddressInfo.tableName);
await txn.delete(WalletInfoAddressMap.tableName);
await txn.delete(WalletInfoAddress.tableName);
+1 -1
View File
@@ -12,7 +12,7 @@ import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:cryptography/cryptography.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:archive/archive_io.dart';
import 'package:archive/archive_io.dart' hide Mac;
import 'package:cw_core/cake_hive.dart';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/entities/encrypt.dart';
+2
View File
@@ -8,6 +8,7 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_backup/backup.dart' as cake_backup;
import 'package:cake_wallet/utils/package_info.dart';
import 'package:crypto/crypto.dart';
import 'package:cw_core/db/sqlite.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
@@ -314,6 +315,7 @@ class BackupServiceV3 extends $BackupService {
// Delete decrypted data file
decryptedData.deleteSync();
await initDb();
}
Future<void> verifyHardwareWallets(String password,
-78
View File
@@ -1,78 +0,0 @@
import 'dart:convert';
import 'package:bech32/bech32.dart';
Uri decodeLNURL(String encodedUrl) {
Uri decodedUri;
/// The URL doesn't have to be encoded at all as per LUD-17: Protocol schemes and raw (non bech32-encoded) URLs.
/// https://github.com/lnurl/luds/blob/luds/17.md
/// Handle non bech32-encoded LNURL
final lud17prefixes = ['lnurlw', 'lnurlc', 'lnurlp', 'keyauth'];
decodedUri = Uri.parse(encodedUrl);
for (final prefix in lud17prefixes) {
if (decodedUri.scheme.contains(prefix)) {
decodedUri = decodedUri.replace(scheme: prefix);
}
}
if (lud17prefixes.contains(decodedUri.scheme)) {
/// If the non-bech32 LNURL is a Tor address, the port has to be http instead of https for the clearnet LNURL so check if the host ends with '.onion' or '.onion.'
decodedUri = decodedUri.replace(
scheme: decodedUri.host.endsWith('onion') ||
decodedUri.host.endsWith('onion.')
? 'http'
: 'https');
} else {
/// Try to parse the input as a lnUrl. Will throw an error if it fails.
final lnUrl = _findLnUrl(encodedUrl);
/// Decode the lnurl using bech32
final bech32 = const Bech32Codec().decode(lnUrl, lnUrl.length);
decodedUri = Uri.parse(utf8.decode(_convert(bech32.data, 5, 8, false)));
}
return decodedUri;
}
/// Parse and return a given lnurl string if it's valid. Will remove
/// `lightning:` from the beginning of it if present.
String _findLnUrl(String input) {
final res = RegExp(
r',*?((lnurl)([0-9]+[a-z0-9]+))',
).allMatches(input.toLowerCase());
if (res.length == 1) {
return res.first.group(0)!;
} else {
throw ArgumentError('Not a valid lnurl string');
}
}
/// Taken from bech32 (bitcoinjs): https://github.com/bitcoinjs/bech32
List<int> _convert(List<int> data, int inBits, int outBits, bool pad) {
var value = 0;
var bits = 0;
final maxV = (1 << outBits) - 1;
final result = <int>[];
for (final dataValue in data) {
value = (value << inBits) | dataValue;
bits += inBits;
while (bits >= outBits) {
bits -= outBits;
result.add((value >> bits) & maxV);
}
}
if (pad) {
if (bits > 0) result.add((value << (outBits - bits)) & maxV);
} else {
if (bits >= inBits) throw Exception('Excess padding');
if ((value << (outBits - bits)) & maxV > 0) {
throw Exception('Non-zero padding');
}
}
return result;
}
@@ -2,9 +2,9 @@ import 'dart:convert';
import 'dart:developer';
import 'package:cake_wallet/core/open_crypto_pay/exceptions.dart';
import 'package:cake_wallet/core/open_crypto_pay/lnurl.dart';
import 'package:cake_wallet/core/open_crypto_pay/models.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/lnurl.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class OpenCryptoPayService {
@@ -59,7 +59,7 @@ class OpenCryptoPayService {
lnUrl = params["lightning"] as String;
}
final url = decodeLNURL(lnUrl);
final url = LNURL.decode(lnUrl);
final params = await _getOpenCryptoPayParams(url);
return OpenCryptoPayRequest(
-470
View File
@@ -1,470 +0,0 @@
import 'package:cw_core/format_fixed.dart';
abstract class PaymentURI {
PaymentURI({required this.amount, required this.address});
final String amount;
final String address;
}
class MoneroURI extends PaymentURI {
MoneroURI({required super.amount, required super.address});
@override
String toString() {
var base = 'monero:$address';
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class HavenURI extends PaymentURI {
HavenURI({required super.amount, required super.address});
@override
String toString() {
var base = 'haven:$address';
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class BitcoinURI extends PaymentURI {
BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
final String pjUri;
@override
String toString() {
final qp = <String, String>{};
if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
if (pjUri.isNotEmpty && !address.startsWith("sp")) {
qp['pjos'] = '0';
qp['pj'] = pjUri;
}
return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
}
}
class LitecoinURI extends PaymentURI {
LitecoinURI({required super.amount, required super.address});
@override
String toString() {
var base = 'litecoin:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class EthereumURI extends PaymentURI {
EthereumURI({required super.amount, required super.address});
@override
String toString() {
var base = 'ethereum:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class BaseURI extends PaymentURI {
BaseURI({required super.amount, required super.address});
@override
String toString() {
var base = 'base:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class ArbitrumURI extends PaymentURI {
ArbitrumURI({required super.amount, required super.address});
@override
String toString() {
var base = 'arbitrum:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class BSCURI extends PaymentURI {
BSCURI({required super.amount, required super.address});
@override
String toString() {
var base = 'bsc:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class BitcoinCashURI extends PaymentURI {
BitcoinCashURI({required super.amount, required super.address});
@override
String toString() {
var base = address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class NanoURI extends PaymentURI {
NanoURI({required super.amount, required super.address});
@override
String toString() {
var base = 'nano:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class PolygonURI extends PaymentURI {
PolygonURI({required super.amount, required super.address});
@override
String toString() {
var base = 'polygon:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class SolanaURI extends PaymentURI {
SolanaURI({required super.amount, required super.address});
@override
String toString() {
var base = 'solana:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class TronURI extends PaymentURI {
TronURI({required super.amount, required super.address});
@override
String toString() {
var base = 'tron:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class WowneroURI extends PaymentURI {
WowneroURI({required super.amount, required super.address});
@override
String toString() {
var base = 'wownero:$address';
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class ZanoURI extends PaymentURI {
ZanoURI({required String amount, required String address})
: super(amount: amount, address: address);
@override
String toString() {
var base = 'zano:' + address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class DecredURI extends PaymentURI {
DecredURI({required String amount, required String address})
: super(amount: amount, address: address);
@override
String toString() {
var base = 'decred:' + address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class DogeURI extends PaymentURI {
DogeURI({required String amount, required String address})
: super(amount: amount, address: address);
@override
String toString() {
var base = 'doge:' + address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class ZcashURI extends PaymentURI {
ZcashURI({required super.amount, required super.address});
@override
String toString() {
var base = 'zcash:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class ERC681URI extends PaymentURI {
final int chainId;
final String? contractAddress;
ERC681URI({
required this.chainId,
required super.address,
required super.amount,
required this.contractAddress,
});
@override
String toString() {
var uri = 'ethereum:';
final targetAddress = contractAddress ?? address;
uri += targetAddress;
if (chainId != 1) {
uri += '@$chainId';
}
if (contractAddress != null) {
uri += '/transfer';
}
final params = <String, String>{};
if (contractAddress != null) {
params['address'] = address;
if (amount.isNotEmpty) {
params['uint256'] = _formatAmountForERC20(amount);
}
} else {
if (amount.isNotEmpty) {
params['value'] = _formatAmountForNative(amount);
}
}
if (params.isNotEmpty) {
uri += '?';
uri += params.entries.map((e) => '${e.key}=${e.value}').join('&');
}
return uri;
}
/// Formats amount for ERC-20 token transfers (in atomic units)
String _formatAmountForERC20(String amount) {
try {
// Convert decimal amount to BigInt (assuming 18 decimals)
final amountDouble = double.parse(amount.replaceAll(',', '.'));
final amountBigInt = BigInt.from(amountDouble * 1e18);
return amountBigInt.toString();
} catch (e) {
// Fallback to original amount if parsing fails
return amount.replaceAll(',', '.');
}
}
/// Formats amount for native ETH payments (in wei using scientific notation)
String _formatAmountForNative(String amount) {
try {
// Convert decimal amount to double for scientific notation
final amountDouble = double.parse(amount.replaceAll(',', '.'));
// Use scientific notation as recommended by ERC-681
return '${amountDouble}e18';
} catch (e) {
// Fallback to original amount if parsing fails
return amount.replaceAll(',', '.');
}
}
factory ERC681URI.fromUri(Uri uri) {
final (isContract, targetAddress) = _getTargetAddress(uri.path);
final chainId = _getChainID(uri.path);
final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress;
final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"];
var formatedAmount = "";
if (amountParam != null) {
final normalized = _normalizeToIntegerWei(amountParam);
formatedAmount = formatFixed(BigInt.parse(normalized), 18);
} else {
formatedAmount = uri.queryParameters["amount"] ?? "";
}
return ERC681URI(
chainId: chainId,
address: address,
amount: formatedAmount,
contractAddress: isContract ? targetAddress : null,
);
}
static int _getChainID(String path) {
return int.parse(RegExp(
r'@\d*',
).firstMatch(path)?.group(0)?.replaceAll("@", "") ??
"1");
}
static (bool, String) _getTargetAddress(String path) {
final targetAddress =
RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false).firstMatch(path)!.group(0)!;
return (path.contains("/"), targetAddress);
}
/// Normalize an input amount into an integer wei string.
///
/// Accepts the following forms:
/// - Integer string: "123000000000000000" → unchanged
/// - Scientific notation: "0.123e18", "1e6" → expanded to integer
/// - Decimal ETH: "0.123456" → shifted by 18 decimals
static String _normalizeToIntegerWei(String input) {
final raw = input.replaceAll(',', '.').trim();
// First we check if it's already a plain integer (basically just a number with no dot, no exponent)
try {
final isPlainInteger = RegExp(r'^[+-]?\d+$').hasMatch(raw) &&
!raw.contains('.') &&
!raw.toLowerCase().contains('e');
if (isPlainInteger) return raw.replaceFirst(RegExp(r'^\+'), '');
// Then we check if it's a scientific notation
final sci = RegExp(r'^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$');
if (sci.hasMatch(raw)) {
final mantissaStr = raw.toLowerCase().split('e')[0];
final exp = int.parse(raw.toLowerCase().split('e')[1]);
return _expandDecimal(mantissaStr, exp);
}
// Lastly, we check if it's a fixed decimal ETH amount, here we shift by 18 to get wei for the amount
if (raw.contains('.')) {
return _expandDecimal(raw, 18);
}
return raw;
} catch (e) {
return raw;
}
// If none of these checks work, we return the raw input
}
/// Expands a decimal string by shifting the decimal point `expShift` places
/// to the right and returns an integer string (digits only, optional leading minus).
/// Examples:
/// _expandDecimal('0.123456', 18) -> '123456000000000000'
/// _expandDecimal('1.2', 3) -> '1200'
static String _expandDecimal(String decimalStr, int expShift) {
var s = decimalStr.trim();
var sign = '';
if (s.startsWith('-') || s.startsWith('+')) {
sign = s[0] == '-' ? '-' : '';
s = s.substring(1);
}
// First we split the integer and fractional parts
final parts = s.split('.');
final intPart = parts[0].isEmpty ? '0' : parts[0];
final fracPart = parts.length > 1 ? parts[1] : '';
final digits = (intPart + fracPart).replaceFirst(RegExp(r'^0+'), '');
final fracLen = fracPart.length;
// Then we calculate the effective shift = desired shift minus existing fractional digits
final shift = expShift - fracLen;
if (shift >= 0) {
final head = digits.isEmpty ? '0' : digits;
final zeros = List.filled(shift, '0').join();
final res = head + zeros;
return sign + (res.isEmpty ? '0' : res);
} else {
// Need to insert a decimal point within digits; return integer by truncating
final cut = digits.length + shift;
if (cut <= 0) {
return '0';
}
final res = digits.substring(0, cut);
return sign + (res.isEmpty ? '0' : res);
}
}
}
+25 -25
View File
@@ -30,7 +30,7 @@ String syncStatusTitle(SyncStatus syncStatus, SyncStatusDisplayMode syncStatusDi
if (eta.isEmpty) {
return S.current.Blocks_remaining('${syncStatus.blocksLeft}');
} else {
return "${syncStatus.formattedProgress()} - ${S.current.eta} $eta";
return "$eta";
}
} else {
return S.current.Blocks_remaining('${syncStatus.blocksLeft}');
@@ -42,54 +42,54 @@ String syncStatusTitle(SyncStatus syncStatus, SyncStatusDisplayMode syncStatusDi
}
if (syncStatus is SyncedSyncStatus) {
return S.current.sync_status_syncronized;
return "";
}
if (syncStatus is FailedSyncStatus) {
if (syncStatus.error != null) {
return syncStatus.error!;
}
return S.current.sync_status_failed_connect;
}
if (syncStatus is NotConnectedSyncStatus) {
return S.current.sync_status_not_connected;
}
if (syncStatus is AttemptingSyncStatus) {
return S.current.sync_status_attempting_sync;
}
if (syncStatus is ConnectingSyncStatus) {
return S.current.sync_status_connecting;
}
if (syncStatus is ConnectedSyncStatus) {
return S.current.sync_status_connected;
return S.current.sync_offline;
}
if (syncStatus is LostConnectionSyncStatus) {
return S.current.sync_status_failed_connect;
return S.current.sync_offline;
}
if (syncStatus is NotConnectedSyncStatus) {
return S.current.sync_not_connected;
}
if (syncStatus is AttemptingSyncStatus) {
return S.current.sync_attempting;
}
if (syncStatus is ConnectingSyncStatus) {
return S.current.sync_connecting;
}
if (syncStatus is ConnectedSyncStatus) {
return S.current.connected;
}
if (syncStatus is UnsupportedSyncStatus) {
return S.current.sync_status_unsupported;
return S.current.sync_unsupported_node;
}
if (syncStatus is TimedOutSyncStatus) {
return S.current.sync_status_timed_out;
return S.current.sync_timed_out;
}
if (syncStatus is SyncronizingSyncStatus) {
return S.current.sync_status_syncronizing;
return S.current.sync_syncing;
}
if (syncStatus is StartingScanSyncStatus) {
return S.current.sync_status_starting_scan(syncStatus.beginHeight.toString());
return "${S.current.sync_starting_scan} (${(syncStatus.beginHeight.toString())})";
}
if (syncStatus is AttemptingScanSyncStatus) {
return S.current.sync_status_attempting_scan;
return "Attempting scan";
}
if (syncStatus is ProcessingSyncStatus) {
+27 -9
View File
@@ -1,6 +1,7 @@
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/lnurl.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
@@ -73,7 +74,7 @@ class UniversalAddressDetector {
// Determine currency from scheme
final currency = CryptoCurrency.fromString(uri.scheme.toLowerCase());
final walletType = cryptoCurrencyToWalletType(currency);
final walletType = cryptoCurrencyOrTokenToWalletType(currency);
final chainId = getChainIdByCryptoCurrency(currency);
return AddressDetectionResult(
@@ -105,21 +106,27 @@ class UniversalAddressDetector {
// Detection patterns ordered by specificity (most specific first)
final detectionPatterns = [
// Lightning Network
// Lightning Network (Bolt11 Invoice format)
_DetectionPattern(
pattern: RegExp(r'^(lnbc|LNBC)[a-km-zA-HJ-NP-Z1-9]{1,}[a-zA-Z0-9]+$'),
pattern: RegExp(r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$', caseSensitive: false),
currency: CryptoCurrency.btcln,
),
// Lightning Network (Bolt12 Offer format)
_DetectionPattern(
pattern: RegExp(r'^(lightning:)?(lno1)[a-z0-9]+$', caseSensitive: false),
currency: CryptoCurrency.btcln,
),
// Lightning Address (email format)
_DetectionPattern(
pattern: RegExp(r'^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'),
currency: CryptoCurrency.btcln,
),
// LNURL format
// Lightning Address (LNURL format)
_DetectionPattern(
pattern: RegExp(r'^(LNURL|lnurl)[a-zA-Z0-9]+$', caseSensitive: false),
pattern: RegExp(r'^(lightning:)?(lnurl)[a-z0-9]+$', caseSensitive: false),
currency: CryptoCurrency.btcln,
),
@@ -152,7 +159,7 @@ class UniversalAddressDetector {
// Bitcoin Cash
_DetectionPattern(
pattern: RegExp(r'^(q|p)[a-z0-9]{41}$'),
pattern: RegExp(r'^([qp])[a-z0-9]{41}$'),
currency: CryptoCurrency.bch,
),
@@ -212,7 +219,7 @@ class UniversalAddressDetector {
// Decred
_DetectionPattern(
pattern: RegExp(r'^(D|T|S)[ksecS][a-km-zA-HJ-NP-Z1-9]+$'),
pattern: RegExp(r'^([DTS])[ksecS][a-km-zA-HJ-NP-Z1-9]+$'),
currency: CryptoCurrency.dcr,
),
@@ -232,8 +239,9 @@ class UniversalAddressDetector {
// Test each pattern in order of specificity
for (final pattern in detectionPatterns) {
if (pattern.pattern.hasMatch(cleanInput)) {
final walletType = cryptoCurrencyToWalletType(pattern.currency);
final walletType = cryptoCurrencyOrTokenToWalletType(pattern.currency);
final chainId = getChainIdByCryptoCurrency(pattern.currency);
final amount = _getAmountFromInvoice(cleanInput, pattern.currency);
return AddressDetectionResult(
address: cleanInput,
@@ -241,6 +249,7 @@ class UniversalAddressDetector {
detectedWalletType: walletType,
isValid: true,
chainId: chainId,
amount: amount,
);
}
}
@@ -251,6 +260,15 @@ class UniversalAddressDetector {
isValid: false,
);
}
static String? _getAmountFromInvoice(String input, CryptoCurrency currency) {
if (currency != CryptoCurrency.btcln) return null;
try {
return getBolt11Amount(input).toString();
} catch (e) {
return null;
}
}
}
class _DetectionPattern {
+6
View File
@@ -6,4 +6,10 @@ extension EnhancedList<T> on Iterable<T> {
return null;
}
}
}
String middleTruncate(String s, int head, int tail) {
if (s.length <= head + tail + 3) return s;
return s.substring(0, head) + '...' + s.substring(s.length - tail);
}
+271 -145
View File
@@ -6,92 +6,63 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/anypay/anypay_api.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart';
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/core/backup_service_v3.dart';
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart';
import 'package:cake_wallet/cake_pay/cake_pay.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_api.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/backup_service_v3.dart';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
import 'package:cake_wallet/core/node_switching_service.dart';
import 'package:cake_wallet/core/reset_service.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/core/trade_monitor.dart';
import 'package:cake_wallet/core/wallet_creation_service.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/core/node_switching_service.dart';
import 'package:cake_wallet/decred/decred.dart';
import 'package:cake_wallet/entities/biometric_auth.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/haven/cw_haven.dart';
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
import 'package:cake_wallet/src/screens/dev/network_requests.dart';
import 'package:cake_wallet/src/screens/dev/qr_tools_page.dart';
import 'package:cake_wallet/src/screens/dev/exchange_provider_logs_page.dart';
import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
import 'package:cake_wallet/src/screens/integrations/deuro/savings_page.dart';
import 'package:cake_wallet/src/screens/settings/background_sync_page.dart';
import 'package:cake_wallet/src/screens/start_tor/start_tor_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart';
import 'package:cake_wallet/store/dashboard/order_filter_store.dart';
import 'package:cake_wallet/themes/core/theme_store.dart';
import 'package:cake_wallet/view_model/dev/monero_background_sync.dart';
import 'package:cake_wallet/view_model/dev/secure_preferences.dart';
import 'package:cake_wallet/view_model/dev/shared_preferences.dart';
import 'package:cake_wallet/view_model/hardware_wallet/bitbox_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/trezor_view_model.dart';
import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cake_wallet/view_model/start_tor_view_model.dart';
import 'package:cake_wallet/zcash/zcash.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart';
import 'package:cake_wallet/entities/wallet_manager.dart';
import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart';
import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart';
import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart';
import 'package:cake_wallet/src/screens/welcome/welcome_page.dart';
import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart';
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/qr_view_data.dart';
import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart';
import 'package:cake_wallet/entities/wallet_manager.dart';
import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/haven/cw_haven.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/decred/decred.dart';
import 'package:cake_wallet/new-ui/new_dashboard.dart';
import 'package:cake_wallet/new-ui/pages/about_page.dart';
import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
import 'package:cake_wallet/new-ui/pages/addresses_page.dart';
import 'package:cake_wallet/new-ui/pages/home_page.dart';
import 'package:cake_wallet/new-ui/pages/send_page.dart';
import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart';
import 'package:cake_wallet/new-ui/pages/receive_page.dart';
import 'package:cake_wallet/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart';
import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart';
import 'package:cake_wallet/new-ui/pages/swap_page.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/reactions/on_authentication_state_change.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/solana/solana.dart';
@@ -99,7 +70,9 @@ import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dar
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/screens/backup/backup_page.dart';
import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
import 'package:cake_wallet/src/screens/buy/webview_page.dart';
import 'package:cake_wallet/src/screens/contact/contact_list_page.dart';
import 'package:cake_wallet/src/screens/contact/contact_page.dart';
@@ -111,12 +84,25 @@ import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart';
import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/balance/balance_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
import 'package:cake_wallet/src/screens/dev/exchange_provider_logs_page.dart';
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
import 'package:cake_wallet/src/screens/dev/network_requests.dart';
import 'package:cake_wallet/src/screens/dev/qr_tools_page.dart';
import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
import 'package:cake_wallet/src/screens/dev/socket_health_logs_page.dart';
import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart';
import 'package:cake_wallet/src/screens/faq/faq_page.dart';
import 'package:cake_wallet/src/screens/integrations/deuro/savings_page.dart';
import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart';
import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart';
import 'package:cake_wallet/src/screens/nano/nano_change_rep_page.dart';
@@ -126,7 +112,9 @@ import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart';
import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart';
import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart';
import 'package:cake_wallet/src/screens/order_details/order_details_page.dart';
import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart';
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart';
@@ -137,20 +125,25 @@ import 'package:cake_wallet/src/screens/restore/restore_options_page.dart';
import 'package:cake_wallet/src/screens/restore/wallet_restore_choose_derivation.dart';
import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart';
import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart';
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart';
import 'package:cake_wallet/src/screens/send/send_page.dart';
import 'package:cake_wallet/src/screens/send/send_template_page.dart';
import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart';
import 'package:cake_wallet/src/screens/settings/background_sync_page.dart';
import 'package:cake_wallet/src/screens/settings/connection_sync_page.dart';
import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/display_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart';
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_settings.dart';
import 'package:cake_wallet/src/screens/settings/other_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
import 'package:cake_wallet/src/screens/settings/security_backup_page.dart';
import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart';
import 'package:cake_wallet/src/screens/settings/silent_payments_logs_page.dart';
import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart';
import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart';
import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart';
import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart';
@@ -158,65 +151,36 @@ import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart
import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_info_page.dart';
import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_qr_page.dart';
import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart';
import 'package:cake_wallet/src/screens/start_tor/start_tor_page.dart';
import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.dart';
import 'package:cake_wallet/src/screens/support/support_page.dart';
import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart';
import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart';
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart';
import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_page.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
import 'package:cake_wallet/view_model/animated_ur_model.dart';
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart';
import 'package:cake_wallet/view_model/anonpay_details_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_api.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
import 'package:cake_wallet/cake_pay/cake_pay.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_account_view_model.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart';
import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart';
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart';
import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart';
import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart';
import 'package:cw_core/nano_account.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/node.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart';
import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart';
import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart';
import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_page.dart';
import 'package:cake_wallet/src/screens/welcome/welcome_page.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart';
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/dashboard/order_filter_store.dart';
import 'package:cake_wallet/store/dashboard/orders_store.dart';
import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart';
import 'package:cake_wallet/store/dashboard/trade_filter_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart';
@@ -227,31 +191,76 @@ import 'package:cake_wallet/store/templates/exchange_template_store.dart';
import 'package:cake_wallet/store/templates/send_template_store.dart';
import 'package:cake_wallet/store/wallet_list_store.dart';
import 'package:cake_wallet/store/yat/yat_store.dart';
import 'package:cake_wallet/themes/core/theme_store.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart';
import 'package:cake_wallet/view_model/animated_ur_model.dart';
import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart';
import 'package:cake_wallet/view_model/anonpay_details_view_model.dart';
import 'package:cake_wallet/view_model/auth_view_model.dart';
import 'package:cake_wallet/view_model/backup_view_model.dart';
import 'package:cake_wallet/view_model/buy/buy_amount_view_model.dart';
import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
import 'package:cake_wallet/view_model/buy/buy_view_model.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_account_view_model.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart';
import 'package:cake_wallet/view_model/dev/exchange_provider_logs_view_model.dart';
import 'package:cake_wallet/view_model/dev/monero_background_sync.dart';
import 'package:cake_wallet/view_model/dev/secure_preferences.dart';
import 'package:cake_wallet/view_model/dev/shared_preferences.dart';
import 'package:cake_wallet/view_model/dev/socket_health_logs_view_model.dart';
import 'package:cake_wallet/view_model/edit_backup_password_view_model.dart';
import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/bitbox_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/trezor_view_model.dart';
import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
import 'package:cake_wallet/view_model/order_details_view_model.dart';
import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
import 'package:cake_wallet/view_model/rescan_view_model.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:cake_wallet/view_model/restore_from_backup_view_model.dart';
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart';
import 'package:cake_wallet/view_model/setup_pin_code_view_model.dart';
import 'package:cake_wallet/view_model/start_tor_view_model.dart';
import 'package:cake_wallet/view_model/support_view_model.dart';
import 'package:cake_wallet/view_model/trade_details_view_model.dart';
import 'package:cake_wallet/view_model/transaction_details_view_model.dart';
@@ -259,19 +268,33 @@ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_details_view_
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart';
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart';
import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart';
import 'package:cake_wallet/view_model/wallet_keys_view_model.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_new_vm.dart';
import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart';
import 'package:cake_wallet/view_model/wallet_restore_view_model.dart';
import 'package:cake_wallet/view_model/wallet_seed_view_model.dart';
import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
import 'package:cake_wallet/view_model/wallet_unlock_loadable_view_model.dart';
import 'package:cake_wallet/view_model/wallet_unlock_verifiable_view_model.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cake_wallet/zano/zano.dart';
import 'package:cake_wallet/zcash/zcash.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/nano_account.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
@@ -280,19 +303,12 @@ import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:trezor_connect/trezor_connect.dart';
import 'buy/kryptonim/kryptonim.dart';
import 'buy/meld/meld_buy_provider.dart';
import 'dogecoin/dogecoin.dart';
import 'new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'src/screens/buy/buy_sell_page.dart';
import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart';
import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
import 'package:cake_wallet/core/trade_monitor.dart';
import 'package:cake_wallet/core/reset_service.dart';
import 'package:cake_wallet/view_model/dev/socket_health_logs_view_model.dart';
import 'package:cake_wallet/src/screens/dev/socket_health_logs_page.dart';
import 'package:cake_wallet/view_model/dev/exchange_provider_logs_view_model.dart';
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
final getIt = GetIt.instance;
@@ -374,7 +390,7 @@ Future<void> setup({
nodeListStore: getIt.get<NodeListStore>(),
themeStore: getIt.get<ThemeStore>()));
getIt.registerSingleton<TradesStore>(
TradesStore(tradesSource: _tradesSource, settingsStore: getIt.get<SettingsStore>()));
TradesStore(tradesSource: _tradesSource, appStore: getIt.get<AppStore>()));
getIt.registerSingleton<OrdersStore>(
OrdersStore(ordersSource: _ordersSource, settingsStore: getIt.get<SettingsStore>()));
getIt.registerFactory(() =>
@@ -441,6 +457,9 @@ Future<void> setup({
newWalletArguments: newWalletArgs,
));
final walletList = await WalletInfo.getAll();
getIt.registerFactory<WalletManager>(
() => WalletManager(
getIt.get<SharedPreferences>(),
@@ -532,7 +551,7 @@ Future<void> setup({
getIt.registerFactory(() => BalanceViewModel(
appStore: getIt.get<AppStore>(),
settingsStore: getIt.get<SettingsStore>(),
fiatConvertationStore: getIt.get<FiatConversionStore>()));
fiatConversionStore: getIt.get<FiatConversionStore>()));
getIt.registerFactory(
() => ExchangeViewModel(
@@ -540,7 +559,6 @@ Future<void> setup({
_tradesSource,
getIt.get<ExchangeTemplateStore>(),
getIt.get<TradesStore>(),
getIt.get<AppStore>().settingsStore,
getIt.get<SharedPreferences>(),
getIt.get<ContactListViewModel>(),
getIt.get<UnspentCoinsListViewModel>(),
@@ -574,8 +592,21 @@ Future<void> setup({
sharedPreferences: getIt.get<SharedPreferences>(),
keyService: getIt.get<KeyService>()));
final walletList = await WalletInfo.getAll();
getIt.registerFactoryParam<CardCustomizerBloc, bool, BitcoinAmountDisplayMode?>(
(lightningMode, displayMode) {
final wallet = getIt.get<AppStore>().wallet!;
return CardCustomizerBloc(wallet,
lightningMode: lightningMode,
displaySats: wallet.type == WalletType.bitcoin && (displayMode == BitcoinAmountDisplayMode.satoshi ||
(displayMode == BitcoinAmountDisplayMode.satoshiForLightning && lightningMode)));
});
getIt.registerFactory<AccountCreationModal>(() => AccountCreationModal(
accountEditOrCreateViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>()));
getIt.registerFactory<LightningUsernameBloc>(
() => LightningUsernameBloc(getIt.get<AppStore>().wallet!));
getIt.registerFactory<AuthService>(
() => AuthService(
secureStorage: getIt.get<SecureStorage>(),
@@ -626,6 +657,13 @@ Future<void> setup({
);
});
getIt.registerFactoryParam<LightningUsernamePage, bool?, void>(
(isSetup, _) => LightningUsernamePage(
isSetup: isSetup ?? false,
themeStore: getIt.get<ThemeStore>(),
lightningUsernameBloc: getIt.get<LightningUsernameBloc>(),
));
getIt.registerFactory<AuthPage>(instanceName: 'login', () {
return AuthPage(getIt.get<AuthViewModel>(), closable: false,
onAuthenticationFinished: (isAuthenticated, AuthPageState authPageState) async {
@@ -742,6 +780,13 @@ Future<void> setup({
addressListViewModel: getIt.get<WalletAddressListViewModel>(),
));
getIt.registerFactory<NewDashboard>(() => NewDashboard(
dashboardViewModel: getIt.get<DashboardViewModel>(),
bottomSheetService: getIt.get<BottomSheetService>(),
));
getIt.registerFactory<NewHomePage>(()=>NewHomePage(dashboardViewModel: getIt.get<DashboardViewModel>(),nftViewModel: getIt.get<NFTViewModel>(),));
getIt.registerFactory<DesktopSidebarWrapper>(() {
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
return DesktopSidebarWrapper(
@@ -802,10 +847,22 @@ Future<void> setup({
getIt.registerFactory<ReceivePage>(
() => ReceivePage(addressListViewModel: getIt.get<WalletAddressListViewModel>()));
getIt.registerFactory<AddressPage>(() => AddressPage(
addressListViewModel: getIt.get<WalletAddressListViewModel>(),
dashboardViewModel: getIt.get<DashboardViewModel>(),
receiveOptionViewModel: getIt.get<ReceiveOptionViewModel>()));
getIt.registerFactory<AddressPage>(
() => AddressPage(
addressListViewModel: getIt<WalletAddressListViewModel>(),
dashboardViewModel: getIt<DashboardViewModel>(),
receiveOptionViewModel: getIt<ReceiveOptionViewModel>(),
),
);
getIt.registerFactoryParam<NewReceivePage, bool?, CryptoCurrency?>((param1, param2) =>
NewReceivePage(
addressListViewModel: getIt.get<WalletAddressListViewModel>(),
receiveOptionViewModel: getIt.get<ReceiveOptionViewModel>(),
dashboardViewModel: getIt.get<DashboardViewModel>(),
lightningMode: param1 ?? false,
initialCurrency: param2));
getIt.registerFactoryParam<WalletAddressEditOrCreateViewModel, WalletAddressListItem?, void>(
(WalletAddressListItem? item, _) =>
@@ -816,9 +873,19 @@ Future<void> setup({
addressEditOrCreateViewModel:
getIt.get<WalletAddressEditOrCreateViewModel>(param1: item)));
getIt.registerFactoryParam<AddressLabelInputPopup, dynamic, void>((dynamic item, _) =>
AddressLabelInputPopup(
walletAddressEditOrCreateViewModel:
getIt.get<WalletAddressEditOrCreateViewModel>(param1: item)));
getIt.registerFactoryParam<ReceiveLabelModal, dynamic, void>((dynamic item, _) =>
ReceiveLabelModal(
walletAddressEditOrCreateViewModel:
getIt.get<WalletAddressEditOrCreateViewModel>(param1: item)));
getIt.registerFactory<SendTemplateViewModel>(() => SendTemplateViewModel(
getIt.get<AppStore>().wallet!,
getIt.get<AppStore>().settingsStore,
getIt.get<AppStore>(),
getIt.get<SendTemplateStore>(),
getIt.get<FiatConversionStore>()));
@@ -849,6 +916,18 @@ Future<void> setup({
walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
));
getIt.registerFactoryParam<NewSendPage, SendPageParams?, void>((params, _) {
params ??= SendPageParams();
return NewSendPage(
sendViewModel: getIt.get<SendViewModel>(param1: params.unspentCoinType),
authService: getIt.get<AuthService>(),
params: params,
contactListViewModel: getIt.get<ContactListViewModel>(),
paymentViewModel: getIt.get<PaymentViewModel>(),
walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
);
});
getIt.registerFactory(
() => SendTemplatePage(sendTemplateViewModel: getIt.get<SendTemplateViewModel>()));
@@ -970,7 +1049,7 @@ Future<void> setup({
getIt.get<NanoAccountEditOrCreateViewModel>(param1: account)));
getIt.registerFactory(() =>
DisplaySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<ThemeStore>()));
DisplaySettingsViewModel(getIt.get<AppStore>(), getIt.get<ThemeStore>()));
getIt.registerFactory(() =>
SilentPaymentsSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
@@ -1009,7 +1088,7 @@ Future<void> setup({
getIt.registerFactory(() => WalletKeysViewModel(getIt.get<AppStore>()));
getIt.registerFactory(() => WalletKeysPage(getIt.get<WalletKeysViewModel>()));
getIt.registerFactory(() => AnimatedURModel(getIt.get<AppStore>()));
getIt.registerFactoryParam<AnimatedURPage, Map<String, String>, void>((Map<String, String> urQr, _) =>
@@ -1022,14 +1101,26 @@ Future<void> setup({
(CryptoCurrency? cur, _) =>
ContactListViewModel(_contactSource, walletList, cur, getIt.get<SettingsStore>()));
getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>((CryptoCurrency? cur, _) =>
ContactListPage(getIt.get<ContactListViewModel>(param1: cur), getIt.get<AuthService>()));
getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, bool?>(
(CryptoCurrency? cur, bool? showAddContact) => ContactListPage(
getIt.get<ContactListViewModel>(param1: cur),
getIt.get<AuthService>(),
showAddButton: showAddContact ?? true,
));
getIt.registerFactoryParam<ContactPage, ContactRecord?, void>(
(ContactRecord? contact, _) => ContactPage(getIt.get<ContactViewModel>(param1: contact)));
getIt.registerFactory(() => AddressListPage(getIt.get<WalletAddressListViewModel>()));
getIt.registerFactoryParam<NewAddressesPage, bool, void>(
(showHidden, _) => NewAddressesPage(
showHidden: showHidden,
addressListViewModel: getIt<WalletAddressListViewModel>(),
dashboardViewModel: getIt<DashboardViewModel>(),
),
);
getIt.registerFactory(() {
final appStore = getIt.get<AppStore>();
return NodeListViewModel(_nodeSource, appStore);
@@ -1066,27 +1157,40 @@ Future<void> setup({
getIt.registerFactory(() => OtherSettingsPage(getIt.get<OtherSettingsViewModel>()));
getIt.registerFactory(()=> AboutPage(appVersion: getIt.get<SettingsStore>().appVersion));
getIt.registerFactory(() => NanoChangeRepPage(
settingsStore: getIt.get<AppStore>().settingsStore,
wallet: getIt.get<AppStore>().wallet!,
));
getIt.registerFactoryParam<NodeCreateOrEditViewModel, WalletType?, bool?>(
(WalletType? type, bool? isPow) => NodeCreateOrEditViewModel(
(isPow ?? false) ? _powNodeSource : _nodeSource,
type ?? getIt.get<AppStore>().wallet!.type,
getIt.get<SettingsStore>()));
getIt.registerFactoryParam<NodeCreateOrEditViewModel, Map<String, dynamic>, void>(
(Map<String, dynamic> args, _) {
final WalletType type = args['type'] as WalletType? ?? getIt.get<AppStore>().wallet!.type;
final bool isPow = args['isPow'] as bool? ?? false;
final Node? editingNode = args['editingNode'] as Node?;
final nodeSourceArgs = isPow ? _powNodeSource : _nodeSource;
return NodeCreateOrEditViewModel(
nodeSourceArgs,
type,
editingNode,
getIt.get<SettingsStore>());
}
);
getIt.registerFactoryParam<NodeCreateOrEditPage, Node?, bool?>(
(Node? editingNode, bool? isSelected) => NodeCreateOrEditPage(
nodeCreateOrEditViewModel: getIt.get<NodeCreateOrEditViewModel>(param2: false),
(Node? editingNode, bool? isSelected) {
final vm = getIt.get<NodeCreateOrEditViewModel>(param1: {'isPow' : false,'editingNode': editingNode});
return NodeCreateOrEditPage(
nodeCreateOrEditViewModel: vm,
editingNode: editingNode,
isSelected: isSelected,
type: getIt.get<AppStore>().wallet!.type));
type: getIt.get<AppStore>().wallet!.type);
});
getIt.registerFactoryParam<PowNodeCreateOrEditPage, Node?, bool?>(
(Node? editingNode, bool? isSelected) => PowNodeCreateOrEditPage(
nodeCreateOrEditViewModel: getIt.get<NodeCreateOrEditViewModel>(param2: true),
nodeCreateOrEditViewModel: getIt.get<NodeCreateOrEditViewModel>(param1: {'isPow' : true, 'editingNode': editingNode}),
editingNode: editingNode,
isSelected: isSelected));
@@ -1111,7 +1215,6 @@ Future<void> setup({
));
getIt.registerFactory<OnRamperBuyProvider>(() => OnRamperBuyProvider(
getIt.get<ThemeStore>(),
wallet: getIt.get<AppStore>().wallet!,
));
@@ -1148,6 +1251,17 @@ Future<void> setup({
return ExchangePage(getIt.get<ExchangeViewModel>(), getIt.get<AuthService>(), paymentRequest);
});
getIt.registerFactoryParam<NewSwapPage, PaymentRequest?, CryptoCurrency?>(
(PaymentRequest? paymentRequest, CryptoCurrency? initialCurrency) {
return NewSwapPage(
getIt.get<ExchangeViewModel>(),
getIt.get<AuthService>(),
paymentRequest,
walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
initialCurrency: initialCurrency,
);
});
getIt.registerFactory(() => ExchangeConfirmPage(tradesStore: getIt.get<TradesStore>()));
getIt.registerFactory(
@@ -1295,7 +1409,7 @@ Future<void> setup({
transactionInfo: transactionInfo,
transactionDescriptionBox: _transactionDescriptionBox,
wallet: wallet,
settingsStore: getIt.get<SettingsStore>(),
appStore: getIt.get<AppStore>(),
sendViewModel: getIt.get<SendViewModel>(),
canReplaceByFee: canReplaceByFee,
);
@@ -1340,6 +1454,11 @@ Future<void> setup({
getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get<CakePayService>()));
getIt.registerFactory(() => CakeFeaturesPage(
dashboardViewModel: getIt.get<DashboardViewModel>(),
cakeFeaturesViewModel: getIt.get<CakeFeaturesViewModel>()));
getIt.registerFactory(() => BackupServiceV3(getIt.get<SecureStorage>(),
_transactionDescriptionBox,
getIt.get<KeyService>(), getIt.get<SharedPreferences>()));
@@ -1435,7 +1554,7 @@ Future<void> setup({
wallet: wallet!,
unspentCoinsInfo: _unspentCoinsInfoSource,
fiatConversationStore: getIt.get<FiatConversionStore>(),
settingsStore: getIt.get<SettingsStore>(),
appStore: getIt.get<AppStore>(),
coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any,
);
});
@@ -1445,6 +1564,13 @@ Future<void> setup({
unspentCoinsListViewModel:
getIt.get<UnspentCoinsListViewModel>(param1: coinTypeToSpendFrom)));
getIt.registerFactoryParam<NewCoinControlPage, UnspentCoinType?, void>(
(coinTypeToSpendFrom, _) => NewCoinControlPage(
unspentCoinsListViewModel:
getIt.get<UnspentCoinsListViewModel>(param1: coinTypeToSpendFrom))
);
getIt.registerFactoryParam<UnspentCoinsDetailsViewModel, UnspentCoinsItem,
UnspentCoinsListViewModel>(
(item, model) =>
@@ -1589,17 +1715,17 @@ Future<void> setup({
getIt.registerFactory(() => DevSharedPreferencesPage(getIt.get<DevSharedPreferences>()));
getIt.registerFactory(() => DevSecurePreferencesPage(getIt.get<DevSecurePreferences>()));
getIt.registerFactory(() => BackgroundSyncLogsViewModel());
getIt.registerFactory(() => DevBackgroundSyncLogsPage(getIt.get<BackgroundSyncLogsViewModel>()));
getIt.registerFactory(() => SocketHealthLogsViewModel());
getIt.registerFactory(() => DevSocketHealthLogsPage(getIt.get<SocketHealthLogsViewModel>()));
getIt.registerFactory(() => DevNetworkRequests());
getIt.registerFactory(() => DevQRToolsPage());
getIt.registerFactory(() => ExchangeProviderLogsViewModel());
@@ -0,0 +1,32 @@
import 'package:cw_core/enumerable_item.dart';
class BitcoinAmountDisplayMode extends EnumerableItem<int> with Serializable<int> {
const BitcoinAmountDisplayMode({required String title, required int raw})
: super(title: title, raw: raw);
static const all = [
BitcoinAmountDisplayMode.satoshi,
BitcoinAmountDisplayMode.satoshiForLightning,
BitcoinAmountDisplayMode.bitcoin,
];
static const satoshiForLightning =
BitcoinAmountDisplayMode(raw: 0, title: 'sats (LN)');
static const bitcoin = BitcoinAmountDisplayMode(raw: 1, title: 'BTC');
static const satoshi = BitcoinAmountDisplayMode(raw: 2, title: 'sats');
static BitcoinAmountDisplayMode deserialize({required int raw}) {
switch (raw) {
case 0:
return satoshiForLightning;
case 1:
return bitcoin;
case 2:
return satoshi;
default:
throw Exception('Unexpected token: $raw for BalanceDisplayMode deserialize');
}
}
@override
String toString() => title;
}
+10 -2
View File
@@ -1,4 +1,4 @@
String calculateFiatAmount({double? price, String? cryptoAmount}) {
String calculateFiatAmount({double? price, String? cryptoAmount, bool raw = false}) {
if (price == null || cryptoAmount == null) {
return '0.00';
}
@@ -13,6 +13,10 @@ String calculateFiatAmount({double? price, String? cryptoAmount}) {
return '0.00';
}
if(raw) {
return result.toStringAsFixed(2);
}
var formatted = '';
final parts = result.toString().split('.');
@@ -34,12 +38,16 @@ String formatWithCommas(String? number) {
final parts = number!.split('.');
final integerPart = parts[0];
final decimalPart = parts.length > 1 ? parts[1] : '';
var decimalPart = parts.length > 1 ? parts[1] : '';
final formattedInteger = integerPart.replaceAllMapped(
RegExp(r'\B(?=(\d{3})+(?!\d))'),
(Match match) => ',',
);
if(decimalPart.length == 1) {
decimalPart = "${decimalPart}0";
}
return decimalPart.isNotEmpty ? '$formattedInteger.$decimalPart' : formattedInteger;
}
+4 -1
View File
@@ -7,7 +7,7 @@ part 'contact.g.dart';
@HiveType(typeId: Contact.typeId)
class Contact extends HiveObject with Keyable {
Contact({required this.name, required this.address, CryptoCurrency? type, DateTime? lastChange})
Contact({required this.name, required this.address, CryptoCurrency? type, DateTime? lastChange, this.displayName = ""})
: lastChange = lastChange ?? DateTime.now() {
if (type != null) {
raw = type.raw;
@@ -29,6 +29,9 @@ class Contact extends HiveObject with Keyable {
@HiveField(3)
DateTime lastChange;
@HiveField(4, defaultValue: "")
String displayName;
CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw);
@override
+3 -1
View File
@@ -1,11 +1,13 @@
import 'package:cw_core/crypto_currency.dart';
abstract class ContactBase {
ContactBase(this.name, this.address, this.type);
ContactBase(this.name, this.address, this.type, {this.displayName = ""});
String name;
String address;
String displayName;
CryptoCurrency type;
}
+5
View File
@@ -14,6 +14,7 @@ abstract class ContactRecordBase extends Record<Contact> with Store implements C
: name = original.name,
address = original.address,
type = original.type,
displayName = original.displayName,
lastChange = original.lastChange,
super(source, original);
@@ -29,6 +30,10 @@ abstract class ContactRecordBase extends Record<Contact> with Store implements C
@observable
CryptoCurrency type;
@override
@observable
String displayName;
DateTime? lastChange;
@override
+23 -13
View File
@@ -1,30 +1,35 @@
import 'dart:convert';
import 'dart:io' show Directory, File, Platform;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/entities/balance_display_mode.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/fs_migration.dart';
import 'package:cake_wallet/entities/haven_seed_store.dart';
import 'package:cake_wallet/entities/node_list.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/db/sqlite.dart';
import 'package:cw_core/node.dart';
import 'package:cake_wallet/entities/sync_status_display_mode.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:hive/hive.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/node.dart';
import 'package:cake_wallet/entities/balance_display_mode.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/node_list.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/fs_migration.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:hive/hive.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/erc20_token.dart';
@@ -570,7 +575,6 @@ Future<void> defaultSettingsMigration(
type: WalletType.zcash,
currentNodePreferenceKey: PreferencesKey.currentZcashNodeIdKey,
);
break;
case 56:
await sharedPreferences.setString(
PreferencesKey.syncStatusDisplayMode, SyncStatusDisplayMode.blocksRemaining.name);
@@ -586,6 +590,12 @@ Future<void> defaultSettingsMigration(
currentNodePreferenceKey: PreferencesKey.currentBscNodeIdKey,
);
break;
case 58: // BalanceCardStyleSettings no-op (handled in sqlite.dart)
case 59: // WalletInfo.receiveInfoboxDismissed no-op (handled in sqlite.dart)
case 60: // BalanceCardStyleSettings.cardOrder no-op (handled in sqlite.dart)
// Do not migrate SQLite here, do that in sqlite.dart in order to prevent runtime
// errors, missing row and missing tables.
break;
default:
break;
}
-28
View File
@@ -1,28 +0,0 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/services.dart';
extension StringExtension on String {
String capitalized() {
return "${this[0].toUpperCase()}${this.substring(1)}";
}
}
Future<String> generateName() async {
final randomThing = Random();
final adjectiveStringRaw =
await rootBundle.loadString('assets/text/Wallet_Adjectives.txt');
final nounStringRaw =
await rootBundle.loadString('assets/text/Wallet_Nouns.txt');
final ls = LineSplitter();
final adjectives = ls.convert(adjectiveStringRaw);
final nouns = ls.convert(nounStringRaw);
final chosenAdjective = adjectives[randomThing.nextInt(adjectives.length)];
final chosenNoun = nouns[randomThing.nextInt(nouns.length)];
final returnString =
chosenAdjective.capitalized() + ' ' + chosenNoun.capitalized();
return returnString;
}
+1 -1
View File
@@ -9,7 +9,7 @@ class LanguageService {
'es': 'Español (Spanish)',
'fa': 'فارسی (Persian)',
'fr': 'Français (French)',
'gn': 'Guarani (Guarani)',
// 'gn': 'Guarani (Guarani)',
'hi': 'हिंदी (Hindi)',
'ja': '日本 (Japanese)',
'ko': '한국어 (Korean)',
+74
View File
@@ -0,0 +1,74 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/lnurl.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class LNUrlPayRecord {
LNUrlPayRecord({
required this.address,
required this.name,
});
final String name;
final String address;
static Future<String?> checkWellKnownUsername(String username, CryptoCurrency currency) async {
if (![CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) return null;
// split the string by the @ symbol:
try {
final List<String> splitStrs = username.split("@");
String name = splitStrs.first.toLowerCase();
final String domain = splitStrs.last;
if (splitStrs.length == 3) {
// for username like @alice@domain.org instead of alice@domain.org
name = splitStrs[1];
}
if (name.isEmpty) {
name = "_";
}
final expectedUrl = "https://$domain/.well-known/lnurlp/$name";
final response = await ProxyWrapper().get(
clearnetUri: Uri.parse(expectedUrl),
headers: <String, String>{"Accept": "application/json"},
);
if (response.statusCode == 200) {
return LNURL.encode(expectedUrl);
}
} catch (e) {
printV("error checking well-known username: $e");
}
return null;
}
static String formatDomainName(String name) {
String formattedName = name;
if (name.contains("@")) {
formattedName = name.replaceAll("@", ".");
}
return formattedName;
}
static Future<LNUrlPayRecord?> fetchAddressAndName({
required String formattedName,
required CryptoCurrency currency,
}) async {
final name = formattedName;
printV("formattedName: $formattedName");
final address = await checkWellKnownUsername(formattedName, currency);
if (address == null) {
return null;
}
return LNUrlPayRecord(address: address, name: name);
}
}
+65
View File
@@ -0,0 +1,65 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
class NewMainActions {
final Key key;
final String Function(BuildContext context) name;
final String image;
final VoidCallback onTap;
final bool Function(DashboardViewModel viewModel)? isEnabled;
final bool Function(DashboardViewModel viewModel)? canShow;
NewMainActions._({
required this.key,
required this.name,
required this.image,
required this.onTap,
this.isEnabled,
this.canShow,
});
static List<NewMainActions> all = [
homeAction,
walletsAction,
contactsAction,
appsAction,
//chartsAction,
];
static NewMainActions homeAction = NewMainActions._(
name: (context) => S.of(context).home,
image: 'assets/new-ui/navbar/home.svg',
key: ValueKey('dashboard_page_home_action_button_key'),
onTap: () {},
);
static NewMainActions walletsAction = NewMainActions._(
name: (context) => S.of(context).wallets,
image: 'assets/new-ui/navbar/wallets.svg',
key: ValueKey('dashboard_page_wallets_action_button_key'),
onTap: () {},
);
static NewMainActions contactsAction = NewMainActions._(
name: (context) => S.of(context).contacts,
image: 'assets/new-ui/navbar/contacts.svg',
key: ValueKey('dashboard_page_contacts_action_button_key'),
onTap: () {},
);
static NewMainActions appsAction = NewMainActions._(
name: (context) => S.of(context).apps,
image: 'assets/new-ui/navbar/apps.svg',
key: ValueKey('dashboard_page_apps_action_button_key'),
canShow: (dashboardVM) => dashboardVM.showApps,
onTap: () {},
);
// static NewMainActions chartsAction = NewMainActions._(
// name: (context) => 'Charts', //TODO S.of(context).charts,
// image: 'assets/new-ui/navbar/charts.svg',
// key: ValueKey('dashboard_page_charts_action_button_key'),
// onTap: () {},
// );
}
@@ -0,0 +1,22 @@
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:flutter/material.dart';
class ListItemCheckbox extends ListItem {
const ListItemCheckbox({
required super.keyValue,
required super.label,
this.subtitle,
this.iconPath,
this.onTap,
this.showArrow = false,
required this.value,
required this.onChanged,
});
final ValueChanged<bool> onChanged;
final VoidCallback? onTap;
final bool value;
final String? subtitle;
final String? iconPath;
final bool showArrow;
}
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
abstract class ListItem {
const ListItem({
required this.keyValue,
required this.label,
this.isFirstInSection = false,
this.isLastInSection = false,
});
final String keyValue;
final String label;
final bool isFirstInSection;
final bool isLastInSection;
BorderRadius get radius => BorderRadius.vertical(
top: Radius.circular(isFirstInSection ? 16 : 0),
bottom: Radius.circular(isLastInSection ? 16 : 0),
);
}
@@ -0,0 +1,14 @@
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:flutter/material.dart';
class ListItemDropdown extends ListItem {
const ListItemDropdown({
required super.keyValue,
required super.label,
this.trailingText,
required this.onTap,
});
final String? trailingText;
final VoidCallback onTap;
}
@@ -0,0 +1,28 @@
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:flutter/material.dart';
class ListItemRegularRow extends ListItem {
const ListItemRegularRow({
required super.keyValue,
required super.label,
this.subtitle,
this.trailingText,
this.iconPath,
this.onTap,
this.trailingIconPath,
this.showArrow = true,
this.truncateTrailingText = false,
this.foregroundColor,
this.trailingIconSize
});
final String? subtitle;
final String? trailingText;
final String? iconPath;
final String? trailingIconPath;
final VoidCallback? onTap;
final bool showArrow;
final bool truncateTrailingText;
final Color? foregroundColor;
final double? trailingIconSize;
}
@@ -0,0 +1,16 @@
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:flutter/material.dart';
class ListItemSelector extends ListItem {
const ListItemSelector({
required this.options,
required super.keyValue,
required super.label,
this.trailingText,
required this.onTap,
});
final String? trailingText;
final VoidCallback onTap;
final List<String> options;
}
@@ -0,0 +1,22 @@
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:flutter/material.dart';
class ListItemTextField extends ListItem {
const ListItemTextField({
required super.keyValue,
required super.label,
this.initialValue,
this.validator,
this.onChanged,
this.onFieldSubmitted,
this.focusNode
});
final String? initialValue;
final FormFieldValidator<String>? validator;
final ValueChanged<String>? onChanged;
final ValueChanged<String>? onFieldSubmitted;
final FocusNode? focusNode;
}
@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
class ListItemToggle extends ListItem {
const ListItemToggle({
required super.keyValue,
required super.label,
required this.value,
required this.onChanged,
this.leadingEndWidget,
});
final ValueChanged<bool> onChanged;
final Widget? leadingEndWidget;
final bool value;
}
@@ -2,6 +2,7 @@ import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/emoji_string_extension.dart';
import 'package:cake_wallet/entities/ens_record.dart';
import 'package:cake_wallet/entities/lnurlpay_record.dart';
import 'package:cake_wallet/entities/fio_address_provider.dart';
import 'package:cake_wallet/entities/openalias_record.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
@@ -386,6 +387,14 @@ class AddressResolver {
return ParsedAddress.fetchWellKnownAddress(address: record.address, name: text);
}
}
if (walletType == WalletType.bitcoin) {
final record =
await LNUrlPayRecord.fetchAddressAndName(formattedName: text, currency: currency);
if (record != null) {
return ParsedAddress.fetchLNUrlPayAddress(address: record.address, name: text);
}
}
}
if (!text.startsWith('@') && text.contains('@') && !text.contains('.')) {
+10 -1
View File
@@ -16,7 +16,8 @@ enum ParseFrom {
wellKnown,
zanoAlias,
zcashAddress,
bip353
bip353,
lnurlpay,
}
class ParsedAddress {
@@ -184,6 +185,14 @@ class ParsedAddress {
);
}
factory ParsedAddress.fetchLNUrlPayAddress({required String address, required String name}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.lnurlpay,
);
}
final List<String> addresses;
final String name;
final String description;
+5
View File
@@ -26,6 +26,7 @@ class PreferencesKey {
static const currentZcashNodeIdKey = 'current_node_id_zec';
static const currentTransactionPriorityKeyLegacy = 'current_fee_priority';
static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
static const displayAmountsInSatoshi = 'display_amounts_in_satoshi';
static const shouldSaveRecipientAddressKey = 'save_recipient_address';
static const isAppSecureKey = 'is_app_secure';
static const disableTradeOption = 'disable_buy';
@@ -62,6 +63,7 @@ class PreferencesKey {
static const zanoTransactionPriority = 'current_fee_priority_zano';
static const wowneroTransactionPriority = 'current_fee_priority_wownero';
static const decredTransactionPriority = 'current_fee_priority_decred';
static const zcashTransactionPriority = 'current_fee_priority_decred';
static const customBitcoinFeeRate = 'custom_electrum_fee_rate';
static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay';
static const mwebCardDisplay = 'mwebCardDisplay';
@@ -105,6 +107,8 @@ class PreferencesKey {
static const showPayjoinCard = 'show_payjoin_card';
static const showCameraConsent = 'show_camera_consent';
static const showDecredInfoCard = 'show_decred_info_card';
static const forceDecentralizedExchanges = 'force_decentralized_exchanges';
static const decentralizedExchangesPromptDismissed = 'decentralized_exchanges_prompt_dismissed';
static String moneroWalletUpdateV1Key(String name) =>
'${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
@@ -131,4 +135,5 @@ class PreferencesKey {
static const enableAutomaticNodeSwitching = 'enable_automatic_node_switching';
static const syncStatusDisplayMode = 'sync_status_display_mode';
static const backgroundImage = 'background_image';
static const mwebAdDismissed = "mweb_ad_dismissed";
}
+4
View File
@@ -17,4 +17,8 @@ class WalletContact implements ContactBase {
/// Wallet type of the wallet this contact belongs to
/// Used for EVM chain filtering
final WalletType? walletType;
String get displayName => "";
set displayName(String _) {}
}
+20 -1
View File
@@ -238,6 +238,13 @@ class CWEVM extends EVM {
) =>
(wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount);
@override
Future<BigInt?> getAllowance(
WalletBase wallet,
String tokenContract,
String spender) =>
(wallet as EVMChainWallet).getAllowance(tokenContract, spender);
@override
Future<PendingTransaction> createTokenApproval(
WalletBase wallet,
@@ -265,14 +272,18 @@ class CWEVM extends EVM {
String to,
String dataHex,
BigInt valueWei,
TransactionPriority? priority, {
TransactionPriority? priority,{
bool useBlinkProtection = true,
String? sourceTokenAddress,
BigInt? sourceTokenAmount,
}) =>
(wallet as EVMChainWallet).createCallDataTransaction(
to,
dataHex,
valueWei,
priority as EVMChainTransactionPriority?,
sourceTokenAddress,
sourceTokenAmount,
useBlinkProtection: useBlinkProtection,
);
@@ -468,6 +479,14 @@ class CWEVM extends EVM {
return _registry.getWalletTypeByChainId(chainId);
}
@override
BigInt? getERC20AvailableBalance(Object balance) {
if(balance is EVMChainERC20Balance) {
return balance.balance;
}
return null;
}
@override
List<ChainInfo> getAllChains() {
final allChains = _registry.getAllChains();
+19 -18
View File
@@ -2,46 +2,47 @@ import 'package:cw_core/enumerable_item.dart';
class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<int> {
const ExchangeProviderDescription(
{required String title, required int raw, required this.image, this.horizontalLogo = false})
{required String title, required int raw, required this.image, this.horizontalLogo = false, required this.isCentralized})
: super(title: title, raw: raw);
final bool horizontalLogo;
final String image;
final bool isCentralized;
static const xmrto =
ExchangeProviderDescription(title: 'XMR.TO', raw: 0, image: 'assets/images/xmrto.png');
ExchangeProviderDescription(title: 'XMR.TO', raw: 0, image: 'assets/new-ui/trade_providers/xmrto.svg', isCentralized: true);
static const changeNow =
ExchangeProviderDescription(title: 'ChangeNOW', raw: 1, image: 'assets/images/changenow.png');
ExchangeProviderDescription(title: 'ChangeNOW', raw: 1, image: 'assets/new-ui/trade_providers/changenow.svg', isCentralized: true);
static const morphToken =
ExchangeProviderDescription(title: 'MorphToken', raw: 2, image: 'assets/images/morph.png');
ExchangeProviderDescription(title: 'MorphToken', raw: 2, image: 'assets/new-ui/trade_providers/morph.svg', isCentralized: true);
static const sideShift =
ExchangeProviderDescription(title: 'SideShift', raw: 3, image: 'assets/images/sideshift.png');
ExchangeProviderDescription(title: 'SideShift', raw: 3, image: 'assets/new-ui/trade_providers/sideshift.svg', isCentralized: true);
static const simpleSwap =
ExchangeProviderDescription(title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png');
ExchangeProviderDescription(title: 'SimpleSwap', raw: 4, image: 'assets/new-ui/trade_providers/simpleswap.svg', isCentralized: true);
static const trocador =
ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png');
ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/new-ui/trade_providers/trocador.svg', isCentralized: true);
static const exolix =
ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png');
ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/new-ui/trade_providers/exolix.svg', isCentralized: true);
static const all =
ExchangeProviderDescription(title: 'All trades', raw: 7, image: '');
ExchangeProviderDescription(title: 'All trades', raw: 7, image: '', isCentralized: true);
static const thorChain =
ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/images/thorchain.png');
ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/new-ui/trade_providers/thorchain.svg', isCentralized: true);
static const swapTrade =
ExchangeProviderDescription(title: 'SwapTrade', raw: 9, image: 'assets/images/swap_trade.png');
ExchangeProviderDescription(title: 'SwapTrade', raw: 9, image: 'assets/new-ui/trade_providers/swaptrade.svg', isCentralized: true);
static const letsExchange =
ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/images/letsexchange_icon.svg');
ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/new-ui/trade_providers/letsexchange.svg', isCentralized: true);
static const stealthEx =
ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png');
ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/new-ui/trade_providers/stealthex.svg', isCentralized: true);
static const chainflip =
ExchangeProviderDescription(title: 'Chainflip', raw: 12, image: 'assets/images/chainflip.png');
ExchangeProviderDescription(title: 'Chainflip', raw: 12, image: 'assets/new-ui/trade_providers/chainflip.svg', isCentralized: false);
static const xoSwap =
ExchangeProviderDescription(title: 'XOSwap', raw: 13, image: 'assets/images/xoswap.svg');
ExchangeProviderDescription(title: 'XOSwap', raw: 13, image: 'assets/new-ui/trade_providers/xoswap.svg', isCentralized: true);
static const swapsXyz =
ExchangeProviderDescription(title: 'Swaps.XYZ', raw: 14, image: 'assets/images/swaps_xyz.svg');
ExchangeProviderDescription(title: 'Swaps.XYZ', raw: 14, image: 'assets/new-ui/trade_providers/swaps_xyz.svg', isCentralized: false);
static const nearIntents =
ExchangeProviderDescription(title: 'Near Intents', raw: 15, image: 'assets/images/near.png');
ExchangeProviderDescription(title: 'Near Intents', raw: 15, image: 'assets/new-ui/trade_providers/near-intents.svg', isCentralized: false);
static const jupiter =
ExchangeProviderDescription(title: 'Jupiter', raw: 16, image: 'assets/images/jupiter.png');
ExchangeProviderDescription(title: 'Jupiter', raw: 16, image: 'assets/new-ui/trade_providers/jupiter.svg', isCentralized: false);
static ExchangeProviderDescription deserialize({required int raw}) {
switch (raw) {
@@ -8,7 +8,6 @@ import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_not_found_exception.dart';
import 'package:cake_wallet/exchange/trade_request.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
@@ -55,11 +54,11 @@ class ExolixExchangeProvider extends ExchangeProvider {
if (isFixedRateMode) {
params['coinFrom'] = _normalizeCurrency(to);
params['coinTo'] = _normalizeCurrency(from);
params['coinTo'] = _normalizeCurrency(_overrideFromCryptoCurrency(from));
params['networkFrom'] = _networkFor(to);
params['networkTo'] = _networkFor(from);
} else {
params['coinFrom'] = _normalizeCurrency(from);
params['coinFrom'] = _normalizeCurrency(_overrideFromCryptoCurrency(from));
params['coinTo'] = _normalizeCurrency(to);
params['networkFrom'] = _networkFor(from);
params['networkTo'] = _networkFor(to);
@@ -103,7 +102,7 @@ class ExolixExchangeProvider extends ExchangeProvider {
if (amount == 0) return 0.0;
final params = {
'coinFrom': _normalizeCurrency(from),
'coinFrom': _normalizeCurrency(_overrideFromCryptoCurrency(from)),
'coinTo': _normalizeCurrency(to),
'networkFrom': _networkFor(from),
'networkTo': _networkFor(to),
@@ -180,6 +179,7 @@ class ExolixExchangeProvider extends ExchangeProvider {
},
);
printV(e.toString());
printV(s.toString());
return 0.0;
}
}
@@ -192,8 +192,8 @@ class ExolixExchangeProvider extends ExchangeProvider {
}) async {
final headers = {'Content-Type': 'application/json'};
final body = {
'coinFrom': _normalizeCurrency(request.fromCurrency),
'coinTo': _normalizeCurrency(request.toCurrency),
'coinFrom': _normalizeCurrency(_overrideFromCryptoCurrency(request.fromCurrency)),
'coinTo': _normalizeCurrency(_overrideToCryptoCurrency(request.toCurrency, request.toAddress)),
'networkFrom': _networkFor(request.fromCurrency),
'networkTo': _networkFor(request.toCurrency),
'withdrawalAddress': _normalizeAddress(request.toAddress),
@@ -217,7 +217,7 @@ class ExolixExchangeProvider extends ExchangeProvider {
if (response.statusCode == 400) {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final errors = responseJSON['errors'] as Map<String, String>;
final errors = responseJSON['error'] as Map<String, String>;
final errorMessage = errors.values.join(', ');
ExchangeProviderLogger.logError(
@@ -394,6 +394,8 @@ class ExolixExchangeProvider extends ExchangeProvider {
switch (currency) {
case CryptoCurrency.arb:
return 'ARBITRUM';
case CryptoCurrency.btcln:
return 'LIGHTNING';
default:
return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title;
}
@@ -406,6 +408,18 @@ class ExolixExchangeProvider extends ExchangeProvider {
};
}
CryptoCurrency _overrideFromCryptoCurrency(CryptoCurrency currency) {
if (currency == CryptoCurrency.zec)
return CryptoCurrency.zaddr; // Sending is always shielded zcash
return currency;
}
CryptoCurrency _overrideToCryptoCurrency(CryptoCurrency currency, String address) {
if (RegExp(r'u1[a-zA-Z0-9]{100,300}').hasMatch(address) && currency == CryptoCurrency.zec)
return CryptoCurrency.zaddr; // If the user pastes a unified address use shielded zcash
return currency;
}
String _normalizeCurrency(CryptoCurrency currency) {
switch (currency) {
case CryptoCurrency.nano:
@@ -414,6 +428,8 @@ class ExolixExchangeProvider extends ExchangeProvider {
return 'BTT';
case CryptoCurrency.zec:
return 'ZEC';
case CryptoCurrency.zaddr:
return 'ZEC-SHIELDED';
default:
return currency.title;
}
@@ -17,6 +17,16 @@ import 'package:cw_core/crypto_currency.dart';
class SwapsXyzExchangeProvider extends ExchangeProvider {
SwapsXyzExchangeProvider();
static final List<CryptoCurrency> _notSupportedAsSourceToken = [
CryptoCurrency.sol,
...CryptoCurrency.all.where(
(c) => (c.tag ?? '').toUpperCase() == 'SOL',
),
];
static const _transferSig = '0xa9059cbb';
static const _swapAndExecuteSig = '0x9be111d1';
static final _apiKey = secrets.swapsXyzApiKey;
static const _baseUrl = 'api-v2.swaps.xyz';
static const _getChainList = 'api/getChainList';
@@ -40,8 +50,10 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
@override
bool get isEnabled => true;
// There is an issue with fixed-rate swaps on Swaps.XYZ
// when they don't guarantee that the trade will be created at the requested amount.
@override
bool get supportsFixedRate => true;
bool get supportsFixedRate => false;
@override
ExchangeProviderDescription get description =>
@@ -60,22 +72,23 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
final chains = await _geSupportedChain();
if (chains.isEmpty) throw Exception('Failed to fetch supported chains');
final srcChain = _findChainByCurrency(from, chains);
final dstChain = _findChainByCurrency(to, chains);
final fromToUse = isFixedRateMode ? to : from;
final toToUse = isFixedRateMode ? from : to;
final srcChain = _findChainByCurrency(fromToUse, chains);
final dstChain = _findChainByCurrency(toToUse, chains);
await _ensureTokensCached(
fromChain: srcChain, toChain: dstChain, from: from, to: to);
fromChain: srcChain, toChain: dstChain, from: fromToUse, to: toToUse);
final srcToken = _getTokenAddress(currency: from, chain: srcChain);
final dstToken = _getTokenAddress(currency: to, chain: dstChain);
final srcToken = _getTokenAddress(currency: fromToUse, chain: srcChain);
final dstToken = _getTokenAddress(currency: toToUse, chain: dstChain);
// parameters to and from are swapped in this endpoint
// i.e. to get limits for FROM -> TO, we request paths for TO -> FROM
final params = {
'srcChainId': '${dstChain.chainId}',
'srcToken': dstToken,
'dstChainId': '${srcChain.chainId}',
'dstToken': srcToken,
'srcChainId': '${srcChain.chainId}',
'srcToken': srcToken,
'dstChainId': '${dstChain.chainId}',
'dstToken': dstToken,
};
final uri = Uri.https(_baseUrl, _getPaths, params);
@@ -86,37 +99,75 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
final body = json.decode(res.body) as Map<String, dynamic>;
final paths = (body['paths'] as List?) ?? const [];
final paths =
(body['paths'] as List? ?? const []).cast<Map<String, dynamic>>();
if (paths.isEmpty) {
throw Exception('No paths for ${from.title} -> ${to.title}');
throw Exception('No paths for ${fromToUse.title} -> ${toToUse.title}');
}
final path0 = paths.first as Map<String, dynamic>;
final tokens = path0['tokens'];
final int requestedDstId = dstChain.chainId;
// If tokens == "all", use path-level amountLimits
if (tokens is String && tokens == 'all') {
final amountLimits = path0['amountLimits'] as Map<String, dynamic>?;
Map<String, dynamic> path = paths.firstWhere(
(p) => p['chainId'] == requestedDstId,
orElse: () => <String, dynamic>{},
);
final min =
double.tryParse(amountLimits?['minAmount']?.toString() ?? '');
final max =
double.tryParse(amountLimits?['maxAmount']?.toString() ?? '');
return Limits(min: min, max: max);
if (path.isEmpty) {
path = paths.firstWhere(
(p) => (p['tokens'] is List) || p['amountLimits'] != null,
orElse: () => paths.first,
);
}
// Otherwise tokens is a list -> find the specific token entry
final tokenList = (tokens as List?) ?? const [];
final token = findTokenBySymbol(title: from.title, tokens: tokenList);
final supportsExactAmountIn =
path['supportsExactAmountIn'] as bool? ?? false;
final supportsExactAmountOut =
path['supportsExactAmountOut'] as bool? ?? false;
if (token == null) {
throw Exception('Token info not found for ${from.title}');
if (isFixedRateMode && !supportsExactAmountOut) {
throw Exception(
'This route does not support fixed receive (exact-amount-out)');
}
if (!isFixedRateMode && !supportsExactAmountIn) {
throw Exception(
'This route does not support exact send (exact-amount-in)');
}
final min = double.tryParse(token['minAmount']?.toString() ?? '');
final max = double.tryParse(token['maxAmount']?.toString() ?? '');
Map<String, dynamic>? useLimits;
if (isFixedRateMode) {
final tokensField = path['tokens'];
useLimits = null;
if (tokensField is List && tokensField.isNotEmpty) {
final tokens = tokensField.cast<Map<String, dynamic>>();
String norm(String s) => s.toUpperCase();
final wantSym = norm(_normalizeCakeNativeTokenName(toToUse.title));
final wantAddr = (dstToken).toLowerCase();
final match = tokens.firstWhere(
(t) {
final sym = norm(t['symbol']?.toString() ?? '');
final addr = (t['address']?.toString() ?? '').toLowerCase();
return sym == wantSym || (addr.isNotEmpty && addr == wantAddr);
},
orElse: () => const <String, dynamic>{},
);
if (match.isNotEmpty) {
useLimits = {
'minAmount': match['minAmount'],
'maxAmount': match['maxAmount'],
};
}
}
} else {
// Floating/Exact-in: use the route-level limits
useLimits = path['amountLimits'] as Map<String, dynamic>?;
}
final min = double.tryParse((useLimits?['minAmount'])?.toString() ?? '');
final max = double.tryParse((useLimits?['maxAmount'])?.toString() ?? '');
return Limits(min: min, max: max);
} catch (e) {
printV('fetchLimits error: $e');
@@ -157,6 +208,15 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
try {
if(_notSupportedAsSourceToken.contains(from)) {
printV(
'fetchRate: source token ${from.title} is not supported as source token');
return 0.0;
}
final chains = await _geSupportedChain();
if (chains.isEmpty) return 0.0;
@@ -248,20 +308,6 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
final dstToken =
_getTokenAddress(currency: request.toCurrency, chain: dstChain);
// Optional: ensure path supports exact-out before attempting fixed rate.
if (isFixedRateMode) {
final path = await _pickPath(
srcChainId: srcChain.chainId,
srcToken: srcToken,
dstChainId: dstChain.chainId,
dstToken: dstToken,
);
if (path == null || !path.supportsExactOut) {
throw Exception(
'This route does not support fixed receive (exact-amount-out)');
}
}
final amountStr = isFixedRateMode ? request.toAmount : request.fromAmount;
final rawAmount = double.tryParse(amountStr) ?? 0.0;
if (rawAmount <= 0) throw Exception('Invalid amount');
@@ -297,12 +343,28 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
final data = json.decode(res.body) as Map<String, dynamic>;
final txId = data['txId'] as String? ?? '';
final vmId = data['vmId'] as String? ?? '';
final txObj = (data['tx'] as Map?) ?? const {};
final txTo = txObj['to']?.toString();
final chainId = txObj['chainId']?.toString();
final routerData = txObj['data']?.toString();
// Allow only:
// - null (native / deposit-address flow)
// - '0x' (no call data)
// - ERC20 transfer(0xa9059cbb) selector
// - swapAndExecute(0x9be111d1) selector
final isAllowed = routerData == null ||
routerData == '0x' ||
_decodeMethodSelector(routerData) == _transferSig ||
_decodeMethodSelector(routerData) == _swapAndExecuteSig;
if (!isAllowed) {
throw Exception('Does not support that method selector');
}
final txValue = txObj['value']?.toString() ?? '0';
final bridgeIds = (data['bridgeIds'] as List?) ?? const [];
@@ -537,57 +599,36 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
}
}
// Ensure tokens for this pair are cached (fills both src & dst chain entries)
// Replace your _ensureTokensCached with this one
Future<void> _ensureTokensCached({
required Chain fromChain,
required Chain toChain,
required CryptoCurrency from,
required CryptoCurrency to,
}) async {
bool needSrc = !_tokensCache.containsKey(fromChain.chainId) ||
final needSrc = !_tokensCache.containsKey(fromChain.chainId) ||
(_tokensCache[fromChain.chainId]?.isEmpty ?? true);
bool needDst = !_tokensCache.containsKey(toChain.chainId) ||
final needDst = !_tokensCache.containsKey(toChain.chainId) ||
(_tokensCache[toChain.chainId]?.isEmpty ?? true);
if (!needSrc && !needDst) return;
// First try: from -> to
await _fetchAndCacheTokens(
srcChainId: fromChain.chainId,
srcToken: from.title.toUpperCase(),
dstChainId: toChain.chainId,
dstToken: to.title.toUpperCase(),
);
// Re-check after first call
needSrc = !_tokensCache.containsKey(fromChain.chainId) ||
(_tokensCache[fromChain.chainId]?.isEmpty ?? true);
needDst = !_tokensCache.containsKey(toChain.chainId) ||
(_tokensCache[toChain.chainId]?.isEmpty ?? true);
// If any still missing, try swapped: to -> from
if (needSrc || needDst) {
await _fetchAndCacheTokens(
srcChainId: toChain.chainId,
srcToken: to.title.toUpperCase(),
dstChainId: fromChain.chainId,
dstToken: from.title.toUpperCase(),
);
if (needSrc) {
await _fetchAndCacheTokens(srcChainId: fromChain.chainId);
}
if (needDst) {
await _fetchAndCacheTokens(srcChainId: toChain.chainId);
}
}
// call getPaths and merge tokens into cache keyed by the chainId
Future<void> _fetchAndCacheTokens({
required int srcChainId,
required String srcToken,
required int dstChainId,
required String dstToken,
}) async {
final params = <String, String>{
'srcChainId': '$srcChainId',
'srcToken': srcToken,
'dstChainId': '$dstChainId',
'srcToken': '0x0000000000000000000000000000000000000000',
// Native placeholder
};
final uri = Uri.https(_baseUrl, _getPaths, params);
@@ -597,21 +638,71 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
return;
}
final body = json.decode(res.body) as Map<String, dynamic>;
Map<String, dynamic> body;
try {
body = json.decode(res.body) as Map<String, dynamic>;
} catch (e) {
printV('getPaths JSON decode error: $e');
return;
}
// Always cache the source chain's native token (from body['srcToken'])
final srcTokenJson = body['srcToken'] as Map<String, dynamic>?;
if (srcTokenJson != null) {
final symbol = (srcTokenJson['symbol'] as String? ?? '').toUpperCase();
if (symbol.isNotEmpty) {
final isNative = srcTokenJson['isNative'] == true;
final decimals = (srcTokenJson['decimals'] as num?)?.toInt();
// Treat native token as address = null so _getTokenAddress() emits zero-address
final addr = isNative ? null : (srcTokenJson['address'] as String?);
_mergeCache(srcChainId, [
TokenPathInfo(
symbol: symbol,
address: addr,
decimals: decimals,
minAmount: srcTokenJson['minAmount']?.toString(),
maxAmount: srcTokenJson['maxAmount']?.toString(),
),
]);
}
}
final paths = (body['paths'] as List?) ?? const [];
if (paths.isEmpty) return;
final path0 = paths.first as Map<String, dynamic>;
final pathChainId = (path0['chainId'] as num?)?.toInt();
final tokens = (path0['tokens'] as List?) ?? const [];
for (final path in paths) {
final map = path as Map<String, dynamic>;
final pathChainId = (map['chainId'] as num?)?.toInt();
if (pathChainId == null) continue;
if (pathChainId == null || tokens.isEmpty) return;
final tokensField = map['tokens'];
final parsed = tokens
.map((t) => TokenPathInfo.fromJson(t as Map<String, dynamic>))
.toList();
// Case 1: String "all" -> cache empty list to indicate all tokens supported
if (tokensField is String) {
if (tokensField.toLowerCase() == 'all') {
_tokensCache[pathChainId] =
_tokensCache[pathChainId] ?? <TokenPathInfo>[];
}
continue;
}
_mergeCache(pathChainId, parsed);
// Case 2: List -> parse and merge
if (tokensField is List) {
final parsed = <TokenPathInfo>[];
for (final token in tokensField) {
if (token is Map<String, dynamic>) {
try {
parsed.add(TokenPathInfo.fromJson(token));
} catch (e) {
printV('Token parse error on chain $pathChainId: $e : $token');
}
}
}
if (parsed.isNotEmpty) {
_mergeCache(pathChainId, parsed);
}
}
}
}
// Merge by symbol, prefer entries that have a non-empty address/decimals
@@ -639,52 +730,19 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
_tokensCache[chainId] = bySymbol.values.toList();
}
String _nativeSymbolForChain(Chain chain) {
final network = chain.name.toUpperCase();
switch (network) {
case 'BTC':
case 'BITCOIN':
return 'BTC';
case 'XMR':
case 'MONERO':
return 'XMR';
case 'TRX':
case 'TRON':
return 'TRX';
case 'ETH':
case 'ETHEREUM':
return 'ETH';
case 'LTC':
case 'LITECOIN':
return 'LTC';
case 'DOGE':
case 'DOGECOIN':
return 'DOGE';
case 'XRP':
case 'RIPPLE':
return 'XRP';
case 'XLM':
case 'STELLAR':
return 'XLM';
case 'SOL':
case 'SOLANA':
return 'SOL';
case 'CARDANO':
return 'ADA';
case 'Bitcoin Cash':
return 'BCH';
case 'POLYGON':
return 'POL';
default:
return network;
}
String _normalizeCakeNativeTokenName(String title) {
final name = title.toUpperCase();
return switch (name) {
'ZZEC' => 'ZEC',
_ => name,
};
}
String _getTokenAddress({
required CryptoCurrency currency,
required Chain chain,
}) {
final symbol = currency.title.toUpperCase();
final symbol = _normalizeCakeNativeTokenName(currency.title);
final list = _tokensCache[chain.chainId];
// Try cache hit
@@ -692,31 +750,17 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
for (final t in list) {
if (t.symbol == symbol && t.address != null && t.address!.isNotEmpty) {
return t.address!;
} else if (t.symbol == symbol && (t.address == null)) {
// Native token on this chain
return '0x0000000000000000000000000000000000000000';
}
}
}
// If it's the native coin for this chain, return native placeholder
final nativeSym = _nativeSymbolForChain(chain);
if (nativeSym == symbol) {
return _nativePlaceholderForVm(chain.vmId);
}
// May fail for non-native Alt-VM assets
return symbol;
}
String _nativePlaceholderForVm(String vmId) {
switch (vmId.toLowerCase()) {
case 'evm':
return '0x0000000000000000000000000000000000000000';
case 'alt-vm':
return '0x0000000000000000000000000000000000000000';
default:
return '0x0000000000000000000000000000000000000000';
}
}
Map<String, dynamic>? findTokenBySymbol(
{required String title, required List<dynamic> tokens}) {
final reqSymbol = title.toUpperCase();
@@ -731,7 +775,9 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
Chain _findChainByCurrency(CryptoCurrency cur, List<Chain> chains) {
final network = _normalizeCakeNetwork(cur.tag ?? cur.title);
return chains.firstWhere(
(c) => c.name.toUpperCase() == network,
(c) {
return c.name.toUpperCase() == network;
},
orElse: () => throw Exception('Unsupported chain for ${cur.title}'),
);
}
@@ -768,6 +814,9 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
final s = str ?? '0';
return s.endsWith('n') ? s.substring(0, s.length - 1) : s;
}
String _decodeMethodSelector(String s) =>
(s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : '';
}
class TokenPathInfo {
@@ -91,6 +91,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
}) async {
try {
if (amount == 0) return 0.0;
if(from == CryptoCurrency.btcln || to == CryptoCurrency.btcln) return 0;
final params = <String, dynamic>{};
final body = <String, String>{
@@ -358,7 +358,13 @@ class XOSwapExchangeProvider extends ExchangeProvider {
final payoutAddress = responseJSON['toAddress'] as String;
final depositAddress = responseJSON['payInAddress'] as String;
final refundAddress = responseJSON['fromAddress'] as String;
final depositAmount = _toDouble(amount['value']);
final depositAmountStr = amount['value'].toString();
final parsedAmount = double.tryParse(depositAmountStr);
if (parsedAmount == null || parsedAmount <= 0) {
throw Exception('Invalid deposit amount received from API');
}
final receiveAmount = toAmount['value'] as String;
final status = responseJSON['status'] as String;
final createdAtString = responseJSON['createdAt'] as String;
@@ -386,7 +392,7 @@ class XOSwapExchangeProvider extends ExchangeProvider {
'depositAddress': depositAddress,
'payoutAddress': payoutAddress,
'refundAddress': refundAddress,
'depositAmount': depositAmount,
'depositAmount': depositAmountStr,
'receiveAmount': receiveAmount,
'status': status,
'createdAt': createdAtString,
@@ -405,7 +411,7 @@ class XOSwapExchangeProvider extends ExchangeProvider {
refundAddress: refundAddress,
state: TradeState.deserialize(raw: status),
createdAt: createdAt,
amount: depositAmount.toString(),
amount: depositAmountStr,
receiveAmount: receiveAmount.toString(),
payoutAddress: payoutAddress,
extraId: extraId,
@@ -499,7 +505,13 @@ class XOSwapExchangeProvider extends ExchangeProvider {
final amount = responseJSON['amount'] as Map<String, dynamic>;
final toAmount = responseJSON['toAmount'] as Map<String, dynamic>;
final orderId = responseJSON['id'] as String;
final depositAmount = amount['value'] as String;
final depositAmountStr = amount['value'].toString();
final parsedAmount = double.tryParse(depositAmountStr);
if (parsedAmount == null || parsedAmount <= 0) {
throw Exception('Invalid deposit amount received from API');
}
final receiveAmount = toAmount['value'] as String;
final depositAddress = responseJSON['payInAddress'] as String;
final payoutAddress = responseJSON['toAddress'] as String;
@@ -526,7 +538,7 @@ class XOSwapExchangeProvider extends ExchangeProvider {
refundAddress: refundAddress,
state: TradeState.deserialize(raw: status),
createdAt: createdAt,
amount: depositAmount,
amount: depositAmountStr,
receiveAmount: receiveAmount,
payoutAddress: payoutAddress,
extraId: extraId,
@@ -554,14 +566,4 @@ class XOSwapExchangeProvider extends ExchangeProvider {
}
return null;
}
double _toDouble(dynamic value) {
if (value is int) {
return value.toDouble();
} else if (value is double) {
return value;
} else {
return 0.0;
}
}
}
+1 -1
View File
@@ -1,9 +1,9 @@
import 'package:cake_wallet/entities/generate_name.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/format_amount.dart';
import 'package:cw_core/generate_name.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
+16 -10
View File
@@ -1,17 +1,19 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/app_scroll_behavior.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/background_sync.dart';
import 'package:cake_wallet/core/node_switching_service.dart';
import 'package:cake_wallet/core/reset_service.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/core/trade_monitor.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/default_settings_migration.dart';
import 'package:cake_wallet/entities/get_encryption_key.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/entities/haven_seed_store.dart';
import 'package:cake_wallet/entities/language_service.dart';
import 'package:cake_wallet/entities/template.dart';
@@ -20,39 +22,43 @@ import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/locales/locale.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/reactions/bootstrap.dart';
import 'package:cake_wallet/router.dart' as Router;
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/root/root.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/test_asset_bundles.dart';
import 'package:cake_wallet/themes/utils/theme_provider.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/zcash/zcash.dart';
import 'package:cw_core/address_info.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/db/sqlite.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/key.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/spl_token.dart';
import 'package:cw_core/tron_token.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/db/sqlite.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/utils/tor/abstract.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/window_size.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -61,11 +67,8 @@ import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:hive/hive.dart';
import 'package:cw_core/root_dir.dart';
import 'package:quick_actions/quick_actions.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cw_core/window_size.dart';
import 'package:logging/logging.dart';
import 'package:cake_wallet/core/trade_monitor.dart';
import 'package:cake_wallet/core/reset_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:trezor_connect/trezor_connect.dart';
final navigatorKey = GlobalKey<NavigatorState>();
@@ -321,7 +324,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
payjoinSessionSource: payjoinSessionSource,
anonpayInvoiceInfo: anonpayInvoiceInfo,
havenSeedStore: havenSeedStore,
initialMigrationVersion: 57,
initialMigrationVersion: 60,
);
}
@@ -437,6 +440,9 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
navigatorObservers: [routeObserver, appRouteObserver],
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
child: child!),
theme: theme,
darkTheme: darkTheme,
themeMode: themeMode,
+5 -1
View File
@@ -48,6 +48,10 @@ class CWMoneroAccountList extends MoneroAccountList {
final moneroWallet = wallet as MoneroWallet;
moneroWallet.walletAddresses.accountList
.setLabelAccount(accountIndex: accountIndex, label: label);
if (accountIndex == moneroWallet.walletAddresses.account?.id) {
moneroWallet.walletAddresses.account = monero_account.Account(
id: accountIndex, label: label, balance: moneroWallet.walletAddresses.account!.balance);
}
}
}
@@ -383,7 +387,7 @@ class CWMonero extends Monero {
Future<int> getCurrentHeight() async {
return monero_wallet_api.getCurrentHeight();
}
@override
bool importKeyImagesUR(Object wallet, String ur) {
final moneroWallet = wallet as MoneroWallet;
+58
View File
@@ -0,0 +1,58 @@
import 'dart:ui';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart';
class LongPressPopupBuilder extends StatelessWidget {
const LongPressPopupBuilder(
{super.key, required this.child, required this.popup, this.spacing = 8});
final Widget child;
final Widget popup;
final double spacing;
@override
Widget build(BuildContext context) {
return GestureDetector(
onLongPress: () {
final RenderBox renderBox = context.findRenderObject() as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
showPopUp(
context: context,
builder: (context) => _buildPopup(context, offset, size),
);
},
child: child,
);
}
Widget _buildPopup(BuildContext context, Offset offset, Size size) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Stack(
children: [
Positioned(
left: offset.dx,
top: offset.dy,
width: size.width,
height: size.height,
child: Material(
color: Colors.transparent,
child: child,
),
),
Positioned(
left: offset.dx * 2,
top: offset.dy + size.height + spacing,
child: Material(
color: Colors.transparent,
child: popup,
),
),
],
),
);
}
}
+111
View File
@@ -0,0 +1,111 @@
import 'package:cake_wallet/router.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/material.dart';
/// allows for full navigation with pages and routes inside a single modal sheet.
/// call this in your modal sheet's builder, passing whatever the modal's first page should be to rootPage.
/// afterwards you can call pushNamed to push other pages, they'll be created inside the modal.
/// you can use the back button (android) or the swipe back gesture (both android and iphone) to go back.
/// calling Navigator.of(context).pop() will pop the page INSIDE the modal sheet.
/// if you want to pop the whole sheet, use Navigator.of(context, rootNavigator: true).pop().
class ModalNavigator extends StatefulWidget {
const ModalNavigator({super.key, required this.rootPage, required this.parentContext, this.heightMode = ModalHeightModes.fullScreen});
final Widget rootPage;
final BuildContext parentContext;
final ModalHeightModes heightMode;
@override
State<ModalNavigator> createState() => _ModalNavigatorState();
}
enum ModalHeightModes {
/// just render as big a modal as possible
fullScreen,
/// after first frame, read height and lock to that height
autoLock,
/// let the content size itself. jumps around when pushing different-sized pages!
natural
}
class _ModalNavigatorState extends State<ModalNavigator> {
final GlobalKey _sheetKey = GlobalKey();
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
double? _sheetHeight;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final context = _sheetKey.currentContext;
if (context == null) return;
if(_sheetHeight != null) return;
final box = context.findRenderObject() as RenderBox;
setState(() {
_sheetHeight = box.size.height;
});
});
}
@override
Widget build(BuildContext context) {
late final double? height;
switch(widget.heightMode) {
case ModalHeightModes.fullScreen:
height = MediaQuery.of(context).size.height; break;
case ModalHeightModes.autoLock:
height = _sheetHeight; break;
case ModalHeightModes.natural:
height = null; break;
}
return Container(
key:_sheetKey,
height: height,
child: Theme(
data: Theme.of(context).copyWith(
pageTransitionsTheme: PageTransitionsTheme(
builders: {
// requested by ui - iphone-style back anim on every platform
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: CupertinoPageTransitionsBuilder(),
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.windows: CupertinoPageTransitionsBuilder(),
},
),
),
child: PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final navigator = _navigatorKey.currentState;
if (navigator != null && await navigator.maybePop()) {
return;
}
Navigator.of(widget.parentContext).pop();
},
child: Navigator(
key: _navigatorKey,
onGenerateRoute: (settings) {
printV(settings.name);
if (settings.name == "/")
return handleRouteWithPlatformAwareness((context) => PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
Navigator.of(widget.parentContext).pop();
},
child: widget.rootPage),
fullscreenDialog: false);
else
return createRoute(settings);
}),
),
),
);
}
}
+169
View File
@@ -0,0 +1,169 @@
import 'dart:io';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/new-ui/pages/home_page.dart';
import 'package:cake_wallet/new-ui/widgets/changelog_modal.dart';
import 'package:cake_wallet/src/screens/contact/contact_list_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/new_main_navbar_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart';
import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart';
import 'package:cake_wallet/src/widgets/vulnerable_seeds_popup.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/utils/version_comparator.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../view_model/dashboard/dashboard_view_model.dart';
class NewDashboard extends StatefulWidget {
NewDashboard({
super.key,
required this.dashboardViewModel,
required this.bottomSheetService,
});
final DashboardViewModel dashboardViewModel;
final BottomSheetService bottomSheetService;
final List<Widget> dashboardPageWidgets = [
getIt.get<NewHomePage>(),
getIt.get<WalletListPage>(),
getIt.get<ContactListPage>(),
getIt.get<CakeFeaturesPage>(),
Placeholder(),
];
@override
State<NewDashboard> createState() => _NewDashboardState();
}
class _NewDashboardState extends State<NewDashboard> {
int _selectedPage = 0;
@override
void initState() {
super.initState();
reaction((_) => widget.dashboardViewModel.appStore.wallet, (_) {
setState(() {
_selectedPage = 0;
});
});
Future.delayed(Duration(milliseconds: 300)).then((_) => _showChangelog(context));
_showVulnerableSeedsPopup(context);
}
@override
Widget build(BuildContext context) {
return BottomSheetListener(
bottomSheetService: widget.bottomSheetService,
child: CupertinoScaffold(
body: Material(
child: Stack(
alignment: Alignment.bottomCenter,
children: [
// widget.dashboardPageWidgets[_selectedPage],
IndexedStack(
index: _selectedPage,
children: widget.dashboardPageWidgets,
),
IgnorePointer(
child: Container(
height: 150,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Theme.of(context).colorScheme.surfaceDim.withAlpha(5),
Theme.of(context).colorScheme.surfaceDim.withAlpha(50),
Theme.of(context).colorScheme.surfaceDim.withAlpha(100),
Theme.of(context).colorScheme.surfaceDim.withAlpha(150),
Theme.of(context).colorScheme.surfaceDim.withAlpha(200),
Theme.of(context).colorScheme.surfaceDim.withAlpha(200),
],
),
),
),
),
SafeArea(
bottom: !(Platform.isIOS),
child: SizedBox(
width: double.infinity,
height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding,
child: AbsorbPointer(
absorbing: true,
child: Container(color: Colors.transparent),
),
),
),
NewMainNavBar(
dashboardViewModel: widget.dashboardViewModel,
selectedIndex: _selectedPage,
onItemTap: (index) {
setState(() {
_selectedPage = index;
});
},
)
],
),
),
),
);
}
void _showChangelog(BuildContext context) async {
final sharedPrefs = await SharedPreferences.getInstance();
final currentAppVersion = VersionComparator.getExtendedVersionNumber(
widget.dashboardViewModel.settingsStore.appVersion);
final lastSeenAppVersion = sharedPrefs.getInt(PreferencesKey.lastSeenAppVersion);
final isNewInstall = sharedPrefs.getBool(PreferencesKey.isNewInstall) ?? true;
if (currentAppVersion != lastSeenAppVersion && !isNewInstall) {
Future<void>.delayed(
Duration(seconds: 1),
() {
if (context.mounted) {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) {
return ChangelogModal(
version: widget.dashboardViewModel.settingsStore.appVersion,
);
});
}
},
);
sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion);
} else if (isNewInstall) {
sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion);
}
}
void _showVulnerableSeedsPopup(BuildContext context) async {
final List<String> affectedWalletNames = await widget.dashboardViewModel.checkAffectedWallets();
if (affectedWalletNames.isNotEmpty) {
Future<void>.delayed(
Duration(seconds: 1),
() {
if (context.mounted) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return VulnerableSeedsPopup(affectedWalletNames);
},
);
}
},
);
}
}
}
+154
View File
@@ -0,0 +1,154 @@
import 'dart:math';
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:url_launcher/url_launcher.dart';
// written by people who happened to read my slack message in 2025
// whoever works on this codebase in the future, feel free to add your own mark here
const List<String> aboutPageEasterEggs = [
"Designed in (S)pain",
"Proudly managing over 🤷‍♂️ XMR",
"The cake is not a lie 🍰",
"I don’t play soccer because I enjoy the sport. I’m just doing it for kicks.",
"Markets in red? Big deal.\nWhat color is the grass outside?",
"Conquered Web3, now working on Web6-7",
"Proud owner of none of your funds\n(we are not impressed)",
"*writing down my seedphrase*\ncake cake cake cake cake cake cake ca...",
"Don't forget to actually use your crypto to pay for stuff in the real world 🙂",
"A chain of blocks? That's preposterous!",
"IOU a hug <3",
"Warning: up to 4.8% programmed by cats",
"We love collecting your data <3\nWe're just really incompetent at it"
];
class AboutPage extends StatefulWidget {
const AboutPage({super.key, required this.appVersion});
final String appVersion;
@override
State<AboutPage> createState() => _AboutPageState();
}
class _AboutPageState extends State<AboutPage> {
static const int easterEggTreshold = 5;
String _bottomText = S.current.payment_made_easy;
int _easterEggCounter = 0;
void _easterEgg() {
_easterEggCounter++;
if(_easterEggCounter == easterEggTreshold) {
setState(() {
_bottomText = aboutPageEasterEggs.elementAt(Random().nextInt(aboutPageEasterEggs.length));
});
}
}
@override
Widget build(BuildContext context) {
return Container(
color: Theme.of(context).colorScheme.surface,
child: Column(children: [
ModalTopBar(
title: S.of(context).about,
leadingIcon: Icon(Icons.arrow_back_ios_new),
onLeadingPressed: Navigator.of(context).pop,
),
Column(
children: [
Column(
spacing: 16,
children: [
SizedBox(),
GestureDetector(
onTap: _easterEgg,
child: SvgPicture.asset(
"assets/new-ui/cake_squircle_icon.svg",
width: 128,
height: 128,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Text(
"Cake Wallet",
style: TextStyle(fontSize: 32, fontWeight: FontWeight.w500),
),
Text(widget.appVersion,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant))
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Wrap(
children: [
Text(
_bottomText,
textAlign: TextAlign.center,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
),
],
),
SizedBox(height: 32),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: NewListSections(sections: {
"": [
ListItemRegularRow(
keyValue: "official website",
label: "Official Website",
onTap: () => launchUrl(Uri.https("cakewallet.com")),
trailingIconPath: "assets/new-ui/link_arrow.svg",
foregroundColor: Theme.of(context).colorScheme.primary,
trailingIconSize: 10),
ListItemRegularRow(
keyValue: "docs",
label: "Cake Docs",
onTap: () => launchUrl(Uri.https("docs.cakewallet.com")),
trailingIconPath: "assets/new-ui/link_arrow.svg",
foregroundColor: Theme.of(context).colorScheme.primary,
trailingIconSize: 10)
],
"2": [
ListItemRegularRow(
keyValue: "gh",
label: "GitHub",
onTap: () => launchUrl(Uri.https("github.com", "cake-tech")),
trailingIconPath: "assets/new-ui/link_arrow.svg",
foregroundColor: Theme.of(context).colorScheme.primary,
trailingIconSize: 10),
ListItemRegularRow(
keyValue: "twitter",
label: "X (Twitter)",
onTap: () => launchUrl(Uri.https("twitter.com", "cakewallet")),
trailingIconPath: "assets/new-ui/link_arrow.svg",
foregroundColor: Theme.of(context).colorScheme.primary,
trailingIconSize: 10),
ListItemRegularRow(
keyValue: "tg",
label: "Telegram",
onTap: () => launchUrl(Uri.https("t.me", "cakewallet")),
trailingIconPath: "assets/new-ui/link_arrow.svg",
foregroundColor: Theme.of(context).colorScheme.primary,
trailingIconSize: 10)
]
}),
)
],
)
]));
}
}
+485
View File
@@ -0,0 +1,485 @@
import 'dart:ui';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/new-ui/pages/card_customizer.dart';
import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
import 'package:cake_wallet/new-ui/widgets/modal_grab_handle.dart';
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:cw_core/balance_card_style_settings.dart';
import 'package:cw_core/generate_name.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class AccountCustomizerListItem {
final BalanceCard card;
final int order;
final AccountListItem accountListItem;
AccountCustomizerListItem(
{required this.card, required this.order, required this.accountListItem});
}
class AccountCustomizer extends StatefulWidget {
const AccountCustomizer(
{super.key,
required this.accountListViewModel,
required this.accountEditOrCreateViewModel,
required this.dashboardViewModel});
final MoneroAccountListViewModel accountListViewModel;
final MoneroAccountEditOrCreateViewModel accountEditOrCreateViewModel;
final DashboardViewModel dashboardViewModel;
@override
State<AccountCustomizer> createState() => _AccountCustomizerState();
}
class _AccountCustomizerState extends State<AccountCustomizer> {
static const double _kStackVisibleFactor = 0.2;
late final double cardWidth = MediaQuery.of(context).size.width * 0.9;
final List<AccountCustomizerListItem> _items = [];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
loadCards();
final activeId = monero!.getCurrentAccount(widget.dashboardViewModel.wallet).id;
for (int i = 0; i < _items.length-1; i++) {
if(_items[i].accountListItem.id == activeId) {
final lastIndex = _items.length - 1;
final temp = _items[i];
_items[i] = _items[lastIndex];
_items[lastIndex] = temp;
saveCardOrder();
widget.dashboardViewModel.loadCardDesigns();
break;
}
}
});
}
@override
void dispose() {
saveCardOrder().then((value) => widget.dashboardViewModel.loadCardDesigns());
super.dispose();
}
void loadCards() {
_items.clear();
final accounts = widget.accountListViewModel.accounts;
for (int i = 0; i < accounts.length; i++) {
final index = widget.dashboardViewModel.cardOrder[i];
if(index == null || index >= accounts.length) {
// db order broken.
reset();
break;
}
_items.add(AccountCustomizerListItem(
card: BalanceCard(
accountName: accounts[index].label,
accountIndex: accounts[index].id,
balance: accounts[index].balance ?? "0.00",
accountBalance: accounts[index].balance ?? "0.00",
designSwitchDuration: Duration.zero,
assetName: widget.accountListViewModel.currency.title,
onCustomizeTapped: (i == accounts.length - 1) ? _openCardCustomizer : null,
selected: i == accounts.length - 1,
width: cardWidth,
design: widget.dashboardViewModel.cardDesigns[index],
),
order: index,
accountListItem: accounts[index]));
}
setState(() {});
}
@override
Widget build(BuildContext context) {
if (_items.isEmpty) return SizedBox.shrink();
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(24))),
child: Column(
children: [
ModalTopBar(
title: S.of(context).wallet_accounts,
leadingIcon: Icon(Icons.close),
onLeadingPressed: Navigator.of(context).maybePop,
trailingIcon: Icon(Icons.refresh),
onTrailingPressed: showResetDialog,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Text(
S.of(context).account_customizer_desc,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
Expanded(
child: Stack(
children: [
ReorderableListView.builder(
padding: EdgeInsets.only(bottom: 196),
scrollController: ModalScrollController.of(context),
onReorder: reorder,
proxyDecorator: (child, index, animation) {
return AnimatedBuilder(
animation: animation,
builder: (context, _) {
final animValue = Curves.easeOutCubic.transform(animation.value);
final scale = lerpDouble(1, 1.05, animValue)!;
return Opacity(
opacity: 1 - animValue.clamp(0.0, 0.1),
child: Center(
child: SizedBox(
width: cardWidth,
child: Transform.scale(
scale: scale,
child: child,
),
),
),
);
},
child: _items[index].card,
);
},
itemCount: _items.length,
itemBuilder: (BuildContext context, int index) {
final card = _items[index].card;
return Container(
key: ValueKey(index),
child: GestureDetector(
onTap: () {
reorder(index, _items.length);
},
child: Align(
alignment: Alignment.topCenter,
heightFactor: _kStackVisibleFactor,
child: card,
),
),
);
},
),
SafeArea(
child: Padding(
padding: EdgeInsets.only(bottom: 50),
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(999999),
onTap: _showAddAccountModal,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(999999)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Icon(
Icons.add,
size: 28,
color: Theme.of(context).colorScheme.primary,
),
Text(
S.of(context).add_account,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500),
)
],
),
),
),
),
),
)))),
],
),
),
],
),
);
}
bool _checkReadyToManage() {
if (widget.dashboardViewModel.status is! SyncedSyncStatus) {
showDialog(
context: context,
builder: (context) => AlertWithOneAction(
alertTitle: S.of(context).wallet_is_syncing,
alertContent: S.of(context).cannot_manage_accounts_during_sync,
buttonText: S.of(context).ok,
buttonAction: Navigator.of(context).pop));
return false;
}
return true;
}
Future<void> _showAddAccountModal() async {
if (!_checkReadyToManage()) {
return;
}
final res = await showCupertinoModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) {
final modal = getIt.get<AccountCreationModal>();
return Material(child: modal);
});
if (res != null && res is bool && res == true) {
await widget.dashboardViewModel.loadCardDesigns();
loadCards();
await saveCardOrder();
}
}
void _openCardCustomizer() {
if (!_checkReadyToManage()) {
return;
}
final bloc = getIt.get<CardCustomizerBloc>(param1: false);
Navigator.of(context).push(CupertinoPageRoute(
builder: (context) {
return BlocProvider(
create: (context) => bloc,
child: Material(
child: CardCustomizer(
cryptoTitle: widget.dashboardViewModel.wallet.currency.fullName ??
widget.dashboardViewModel.wallet.currency.name,
cryptoName: widget.dashboardViewModel.wallet.currency.name,
),
),
);
},
)).then((_) async {
bloc.add(DesignSaved());
await bloc.stream.firstWhere((item) => item is CardCustomizerSaved);
await widget.dashboardViewModel.loadCardDesigns();
loadCards();
});
}
void reorder(int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final AccountCustomizerListItem item = _items.removeAt(oldIndex);
_items.insert(newIndex, item);
});
// necessary to copy all this to keep constant constructor for BalanceCard
for (int i = 0; i < _items.length; i++) {
_items[i] = AccountCustomizerListItem(
card: BalanceCard(
accountName: _items[i].card.accountName,
balance: _items[i].card.balance,
accountIndex: _items[i].card.accountIndex,
accountBalance: _items[i].card.accountBalance,
assetName: _items[i].card.assetName,
designSwitchDuration: _items[i].card.designSwitchDuration,
onCustomizeTapped: (i == _items.length - 1) ? _openCardCustomizer : null,
selected: i == _items.length - 1,
width: _items[i].card.width,
design: _items[i].card.design,
),
order: i,
accountListItem: _items[i].accountListItem);
}
if (newIndex == _items.length - 1 || oldIndex == _items.length - 1) {
widget.accountListViewModel.select(_items[_items.length - 1].accountListItem);
}
}
Future<void> saveCardOrder() async {
for (int i = 0; i < _items.length; i++) {
final idx = _items.indexWhere((element) => element.accountListItem.id == i);
printV("$i: $idx");
await BalanceCardStyleSettings.fromCardDesign(
widget.dashboardViewModel.wallet.walletInfo.internalId,
i,
idx,
_items[idx].card.design)
.insert();
}
}
Future<void> showResetDialog() async {
final res = await showPopUp(
context: context,
builder: (context) {
return AlertWithTwoActions(
alertTitle: S.of(context).reset,
alertContent: S.of(context).card_order_reset_desc,
leftButtonText: S.of(context).yes,
rightButtonText: S.of(context).no,
actionLeftButton: () {
Navigator.of(context).pop(true);
},
actionRightButton: Navigator.of(context).pop);
});
if(res != null && res is bool && res) {
reset();
}
}
Future<void> reset() async {
_items.clear();
final accounts = widget.accountListViewModel.accounts;
for (int i = 0; i < widget.accountListViewModel.accounts.length; i++) {
_items.add(AccountCustomizerListItem(
card: BalanceCard(
accountName: accounts[i].label,
accountIndex: accounts[i].id,
balance: accounts[i].balance ?? "0.00",
accountBalance: accounts[i].balance ?? "0.00",
assetName: widget.accountListViewModel.currency.title,
selected: true,
designSwitchDuration: Duration(milliseconds: 200),
width: cardWidth,
design: widget.dashboardViewModel.cardDesigns[i],
),
order: i,
accountListItem: accounts[i]));
}
saveCardOrder();
setState(() {});
}
}
class AccountCreationModal extends StatefulWidget {
const AccountCreationModal({super.key, required this.accountEditOrCreateViewModel});
final MoneroAccountEditOrCreateViewModel accountEditOrCreateViewModel;
@override
State<AccountCreationModal> createState() => _AccountCreationModalState();
}
class _AccountCreationModalState extends State<AccountCreationModal> {
final TextEditingController _controller = TextEditingController();
bool _loading = false;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(30))),
child: SafeArea(
top: false,
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ModalGrabHandle(),
ModalTopBar(title: S.of(context).create_account),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
spacing: 50,
children: [
SizedBox(),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(16)),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(hintText: S.of(context).account_name),
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: GestureDetector(
onTap: () async {
_controller.text = await generateName();
},
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(5)),
child: SvgPicture.asset(
"assets/new-ui/randomize.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
),
),
)
],
),
),
NewPrimaryButton(
onPressed: () async {
if (_loading) return;
setState(() {
_loading = true;
});
widget.accountEditOrCreateViewModel.label = _controller.text;
await widget.accountEditOrCreateViewModel.save();
Navigator.of(context).pop(true);
},
text: S.of(context).continue_text,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isLoading: _loading,
),
SizedBox(),
],
),
)
],
),
),
));
}
}
+480
View File
@@ -0,0 +1,480 @@
import 'dart:ui';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/new-ui/long_press_popup.dart';
import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
import 'package:cake_wallet/new-ui/widgets/long_press_menu.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_svg/svg.dart';
import 'package:cake_wallet/utils/list_item.dart';
import 'package:mobx/mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class NewAddressesPage extends StatefulWidget {
const NewAddressesPage(
{super.key,
required this.addressListViewModel,
required this.dashboardViewModel,
required this.showHidden});
final WalletAddressListViewModel addressListViewModel;
final DashboardViewModel dashboardViewModel;
final bool showHidden;
@override
State<NewAddressesPage> createState() => _NewAddressesPageState();
}
class _NewAddressesPageState extends State<NewAddressesPage> {
List<ListItem> getItems(List<ListItem> list) {
return list.where((element) {
if (element is WalletAddressListItem) {
if (!element.isHidden && !widget.showHidden) return true;
if (element.isHidden && widget.showHidden) return true;
return false;
}
return false;
}).toList();
}
List<ListItem> items = [];
late final TextEditingController _searchController;
CardDesign? design;
void updateItems() {
setState(() {
items = getItems(widget.addressListViewModel.forceRecomputeItems);
});
}
@override
void initState() {
super.initState();
items = getItems(widget.addressListViewModel.items);
_searchController = TextEditingController()
..addListener(() {
setState(() {});
});
WidgetsBinding.instance.addPostFrameCallback((_) {
reaction((context) => widget.dashboardViewModel.cardDesigns.first, (value) {
setState(() {
design = value;
});
});
});
}
@override
Widget build(BuildContext context) {
final filteredItems = items.where((item) {
if (item is! WalletAddressListItem) return false;
if (_searchController.text.isEmpty) return true;
if (item.name == null || item.name!.isEmpty) {
return (item.address.contains(_searchController.text.toLowerCase()));
}
return (item.name ?? '').toLowerCase().contains(_searchController.text.toLowerCase());
}).toList();
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
child: Column(
spacing: 12.0,
children: [
ModalTopBar(
title: widget.showHidden ? S.of(context).hidden_addresses : S.of(context).addresses,
leadingIcon: Icon(Icons.arrow_back),
onLeadingPressed: Navigator.of(context).pop,
onTrailingPressed: () {}),
Expanded(
child: Stack(
children: [
CustomScrollView(
controller: ModalScrollController.of(context),
slivers: [
if (!widget.showHidden)
SliverToBoxAdapter(
child: Column(
spacing: 16,
children: [
if (widget.dashboardViewModel.type == WalletType.monero ||
widget.dashboardViewModel.type == WalletType.wownero)
Observer(
builder: (_) => AccountPreviewHeader(
dashboardViewModel: widget.dashboardViewModel,
design: design,
)),
Text(
S.of(context).long_press_edit_address,
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
ShowHiddenButton()
],
),
),
SliverPadding(
padding: EdgeInsets.only(bottom: 64),
sliver: SliverList.separated(
itemCount: filteredItems.length,
itemBuilder: (context, index) {
final item = filteredItems[index];
if (item is WalletAddressListItem) {
return Observer(
builder: (_) => AddressRow(
selected: item.address == widget.addressListViewModel.address.address,
first: widget.showHidden && index == 0,
last: index == filteredItems.length - 1,
item: item,
onSelect: () {
widget.addressListViewModel.setAddress(item);
},
onLabelChanged: updateItems,
onAddressHidden: () async {
await widget.addressListViewModel.toggleHideAddress(item);
updateItems();
},
walletType: widget.addressListViewModel.type,
),
);
}
return SizedBox.shrink();
},
separatorBuilder: (_, __) => Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
),
),
),
SliverToBoxAdapter(child: SizedBox(height: 72))
],
),
SafeArea(
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: AddressSearchBox(controller: _searchController)),
),
],
),
)
],
),
);
}
}
class AddressSearchBox extends StatelessWidget {
const AddressSearchBox({super.key, required this.controller});
final TextEditingController controller;
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 18.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(999999),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh.withAlpha(128),
border: Border.all(
color: Theme.of(context).colorScheme.surfaceContainerHighest, width: 1),
borderRadius: BorderRadius.circular(99999)),
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: S.of(context).search,
hintStyle: TextStyle(
fontWeight: FontWeight.w600,
),
prefixIcon: Icon(Icons.search),
filled: false,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(99999),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(99999),
borderSide: BorderSide.none,
),
contentPadding: EdgeInsets.symmetric(vertical: 8),
),
),
),
),
),
),
);
}
}
class AccountPreviewHeader extends StatelessWidget {
AccountPreviewHeader({super.key, required this.dashboardViewModel, required this.design});
final DashboardViewModel dashboardViewModel;
final CardDesign? design;
@override
Widget build(BuildContext context) {
return Container(
height: 64,
width: MediaQuery.of(context).size.width * 0.9,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
spacing: 10,
children: [
Observer(
builder: (_) => BalanceCard(
borderRadius: 5, width: 50, design: design ?? CardDesign.genericDefault),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
monero?.getCurrentAccount(dashboardViewModel.wallet).label ??
wownero?.getCurrentAccount(dashboardViewModel.wallet).label ??
"",
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.primary),
),
Text(
dashboardViewModel.wallet.name,
style: TextStyle(
fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
)
],
),
],
),
Row(
spacing: 12,
children: [
Container(
width: 1,
height: 40,
decoration:
BoxDecoration(color: Theme.of(context).colorScheme.surfaceContainerHigh),
),
Text(dashboardViewModel.balanceViewModel.balances.values.first.availableBalance)
],
)
],
),
),
);
}
}
class AddressRow extends StatelessWidget {
const AddressRow(
{super.key,
required this.selected,
required this.first,
required this.last,
required this.item,
required this.onSelect,
required this.walletType,
required this.onLabelChanged,
required this.onAddressHidden});
final bool selected;
final bool first;
final bool last;
final WalletAddressListItem item;
final VoidCallback onSelect;
final VoidCallback onLabelChanged;
final VoidCallback onAddressHidden;
final WalletType walletType;
@override
Widget build(BuildContext context) {
final hasLabel = item.name != null && item.name!.isNotEmpty;
return GestureDetector(
onTap: onSelect,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: LongPressPopupBuilder(
popup: LongPressMenu(
items: [
LongPressMenuItem(
label: S.of(context).set_label,
iconPath: "assets/new-ui/address_set_label.svg",
onSelected: () async {
Navigator.of(context, rootNavigator: true).pop();
final res = await showPopUp(
context: context,
builder: (context) => getIt.get<AddressLabelInputPopup>(param1: item));
if (res != null) {
onLabelChanged();
}
}),
LongPressMenuItem(
label: item.isHidden ? S.of(context).unhide_address : S.of(context).hide_address,
iconPath: "assets/new-ui/address_hide.svg",
onSelected: () {
Navigator.of(context, rootNavigator: true).pop();
onAddressHidden();
},
color: Theme.of(context).colorScheme.error),
],
),
child: AnimatedContainer(
duration: Duration(milliseconds: 150),
decoration: BoxDecoration(
color: selected
? Theme.of(context).colorScheme.surfaceContainerHigh
: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(
top: Radius.circular(first ? 16 : 0),
bottom: Radius.circular(last ? 16 : 0),
),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
spacing: 8,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
children: [
Row(
spacing: 4,
children: [
if (hasLabel)
Text(
item.name!,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500),
),
Expanded(
child: AddressFormatter.buildSegmentedAddress(
address: item.address,
walletType: walletType,
textAlign: TextAlign.left,
evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
fontWeight: FontWeight.w400,
color: hasLabel
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onSurface,
),
shouldTruncate: (hasLabel)),
),
],
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${S.of(context).transactions}: ${item.txCount}",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
Text("${S.of(context).balance}: ${item.balance}",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant)),
],
)
],
),
),
if (selected) SvgPicture.asset("assets/new-ui/checkmark.svg")
],
),
),
),
),
),
);
}
}
class ShowHiddenButton extends StatelessWidget {
const ShowHiddenButton({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
Material(
child: InkWell(
onTap: () {
Navigator.of(context).pushNamed(Routes.receiveAddresses, arguments: true);
},
child: Container(
height: 48,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).show_hidden_addresses),
RotatedBox(
quarterTurns: 1,
child: SvgPicture.asset("assets/new-ui/dropdown_arrow.svg"))
],
),
),
),
),
),
Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
],
),
);
}
}
+237
View File
@@ -0,0 +1,237 @@
import 'dart:async';
import 'dart:math';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CardCustomizer extends StatefulWidget {
const CardCustomizer({super.key, required this.cryptoTitle, required this.cryptoName});
final String cryptoTitle;
final String cryptoName;
@override
State<CardCustomizer> createState() => _CardCustomizerState();
}
class _CardCustomizerState extends State<CardCustomizer> {
final accountNameController = TextEditingController();
bool editEnabled = false;
late final CardCustomizerBloc bloc;
@override
void initState() {
super.initState();
bloc = context.read<CardCustomizerBloc>();
if (bloc.state is! CardCustomizerNotLoaded) {
editEnabled = bloc.state.accountName.isNotEmpty;
accountNameController.text = bloc.state.accountName;
} else {
late final StreamSubscription sub;
sub = bloc.stream.listen((state) {
if (state is! CardCustomizerNotLoaded) {
setState(() {
editEnabled = state.accountName.isNotEmpty;
});
sub.cancel();
}
});
}
}
@override
Widget build(BuildContext context) {
return BlocListener<CardCustomizerBloc, CardCustomizerState>(
listenWhen: (previous, current) =>
previous.accountName != current.accountName,
listener: (context, state) {
if(accountNameController.text != state.accountName) {
accountNameController.text = state.accountName;
}
},
child: BlocBuilder<CardCustomizerBloc, CardCustomizerState>(
builder: (context, state) {
if (state is CardCustomizerNotLoaded) return SizedBox.shrink();
return PopScope(
child: SingleChildScrollView(
child: SafeArea(
child: Column(
spacing: 25.0,
mainAxisSize: MainAxisSize.min,
children: [
ModalTopBar(
title: editEnabled ? S.of(context).edit_account : S.of(context).edit_card,
leadingIcon: Icon(Icons.close),
// trailingIcon: editEnabled ? Icon(Icons.delete_forever) : null,
onLeadingPressed: () => Navigator.of(context).maybePop(),
// onTrailingPressed: () {},
),
if (editEnabled)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).account_name),
TextField(
maxLength: 32,
decoration: InputDecoration(counterText: ""),
onChanged: (value) {
context.read<CardCustomizerBloc>().add(AccountNameChanged(value));
},
controller: accountNameController,
)
],
),
),
BalanceCard(
width: min(MediaQuery.of(context).size.width * 0.87, 768),
selected: true,
designSwitchDuration: Duration(milliseconds: 300),
accountName:
editEnabled ? state.accountName:"" ,
balance: "0.00",
assetName: state.displaySats ? "sats" : widget.cryptoName,
capitalizeAssetName: !state.displaySats,
design: state.selectedDesign,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).card_style),
Container(
height: 63,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: state.availableDesigns.length,
separatorBuilder: (context, index) {
return SizedBox(width: 8.0);
},
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
context
.read<CardCustomizerBloc>()
.add(CardDesignSelected(index));
},
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(
side: BorderSide(color: ((index == state.selectedDesignIndex )? Theme.of(context).colorScheme.onSurface : Colors.transparent), width: 1),
borderRadius: BorderRadiusGeometry.circular(12),
),
),
child: AnimatedScale(
duration: Duration(milliseconds: 200),
scale: index == state.selectedDesignIndex ? 0.94 : 1,
child: BalanceCard(
width: 96,
borderRadius: 10,
selected: false,
designSwitchDuration: Duration(milliseconds: 300),
design: state.availableDesigns[index],
gradient: state.selectedDesign.gradient,
),
),
),
);
}),
),
],
)),
SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).color),
Container(
width: double.infinity,
child: Wrap(
direction: Axis.horizontal,
spacing: 4, // space between items in a row
runSpacing: 8,
children:
List.generate(state.availableColors.length, (index) {
return Material(
borderRadius: BorderRadius.circular(999999999),
child: InkWell(
borderRadius: BorderRadius.circular(999999999),
onTap: () {
context
.read<CardCustomizerBloc>()
.add(ColorSelected(index));
},
child: Stack(
children: [
AnimatedOpacity(
duration: Duration(milliseconds: 200),
opacity: index == state.selectedColorIndex ? 1 : 0,
child: Container(
width:32,height:32,decoration: BoxDecoration(borderRadius: BorderRadius.circular(99999999),border: Border.all(color:Theme.of(context)
.colorScheme
.onSurface))
),
),
AnimatedScale(
duration: Duration(milliseconds: 200),
scale: index == state.selectedColorIndex ? 0.8 : 1,
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(99999999),
gradient: state.availableColors[index]),
),
),
],
),
),
);
}),
)),
],
),
),
)
],
),
)),
],
),
),
),
);
},
),
);
}
}
+228
View File
@@ -0,0 +1,228 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coin_control_page/coin_control_list_item.dart';
import 'package:cake_wallet/new-ui/widgets/modal_header.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import "package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart";
class NewCoinControlPage extends StatefulWidget {
const NewCoinControlPage({super.key, required this.unspentCoinsListViewModel});
final UnspentCoinsListViewModel unspentCoinsListViewModel;
@override
State<NewCoinControlPage> createState() => _NewCoinControlPageState();
}
class _NewCoinControlPageState extends State<NewCoinControlPage> {
late Future<void> _initialization;
@override
void initState() {
super.initState();
_initialization = widget.unspentCoinsListViewModel.initialSetup();
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: !widget.unspentCoinsListViewModel.isSavingItems,
child: Material(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
child: Column(
children: [
ModalTopBar(
title: "",
onLeadingPressed: Navigator.of(context).pop,
leadingIcon: Icon(Icons.arrow_back_ios_new),
onTrailingPressed: () {}),
FutureBuilder(
future: _initialization,
builder: (context, asyncSnapshot) {
if (asyncSnapshot.connectionState == ConnectionState.waiting) {
return Expanded(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 12,
children: [
CupertinoActivityIndicator(),
Text("${S.of(context).loading}...")
],
)),
);
}
if (asyncSnapshot.hasError)
return Center(child: Text(S.of(context).coin_control_load_failed));
return Expanded(
child: SingleChildScrollView(
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: ModalHeader(
iconPath: "assets/new-ui/settings_row_icons/coin-control.svg",
title: "Coin Control",
message: S.of(context).coin_control_desc),
),
if (widget.unspentCoinsListViewModel.items.isNotEmpty)
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
spacing: 20,
children: [
GestureDetector(
onTap: () {
widget.unspentCoinsListViewModel.toggleSelectAll(true);
},
child: Text(S.of(context).select_all,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 14,
fontWeight: FontWeight.w400)),
),
GestureDetector(
onTap: () {
widget.unspentCoinsListViewModel.toggleSelectAll(false);
},
child: Text(S.of(context).unselect_all,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 14,
fontWeight: FontWeight.w400)),
)
],
),
),
if (widget.unspentCoinsListViewModel.nonFrozenItems.isEmpty &&
widget.unspentCoinsListViewModel.frozenItems.isEmpty) ...[
SizedBox(height: 12),
Center(
child: Text(
S.of(context).no_unspent_coins,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)),
],
Observer(
builder: (_) => widget
.unspentCoinsListViewModel.nonFrozenItems.isEmpty
? SizedBox.shrink()
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: CoinControlListSection(
items: widget.unspentCoinsListViewModel.nonFrozenItems,
unspentCoinsListViewModel:
widget.unspentCoinsListViewModel),
),
),
Observer(
builder: (context) =>
widget.unspentCoinsListViewModel.frozenItems.isEmpty
? SizedBox.shrink()
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
spacing: 10,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 12),
Text(
S.of(context).frozen,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant),
),
CoinControlListSection(
items: widget
.unspentCoinsListViewModel.frozenItems,
unspentCoinsListViewModel:
widget.unspentCoinsListViewModel),
]),
),
),
SizedBox(height: 12)
],
),
),
),
);
})
],
),
),
),
);
}
}
class CoinControlListSection extends StatelessWidget {
const CoinControlListSection(
{super.key, required this.items, required this.unspentCoinsListViewModel});
final List<UnspentCoinsItem> items;
final UnspentCoinsListViewModel unspentCoinsListViewModel;
@override
Widget build(BuildContext context) {
return ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: NeverScrollableScrollPhysics(),
itemCount: items.length,
separatorBuilder: (_, __) => Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
itemBuilder: (_, int index) {
return Observer(builder: (_) {
final item = items[index];
final fiatAmount = unspentCoinsListViewModel.fiatAmounts[item.amount] ?? '';
return GestureDetector(
onTap: () => Navigator.of(context).pushNamed(
Routes.unspentCoinsDetails,
arguments: [item, unspentCoinsListViewModel],
),
child: CoinControlListItem(
note: item.note,
amount: item.amount,
fiatAmount: fiatAmount,
address: item.address,
isSending: item.isSending,
isFrozen: item.isFrozen,
isChange: item.isChange,
isSilentPayment: item.isSilentPayment,
isLoading: item.isBeingSaved,
isFirst: index == 0,
isLast: index == items.length - 1,
onCheckBoxTap: item.isFrozen
? null
: () async {
item.isSending = !item.isSending;
await unspentCoinsListViewModel.saveUnspentCoinInfo(item);
},
),
);
});
},
);
}
}
+227
View File
@@ -0,0 +1,227 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/new-ui/modal_navigator.dart';
import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
import 'package:cake_wallet/new-ui/pages/card_customizer.dart';
import 'package:cake_wallet/new-ui/pages/settings_page.dart';
import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/action_row/coin_action_row.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_history_section.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/cards/cards_view.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class NewHomePage extends StatefulWidget {
NewHomePage({super.key, required this.dashboardViewModel, required this.nftViewModel});
final DashboardViewModel dashboardViewModel;
final NFTViewModel nftViewModel;
@override
State<NewHomePage> createState() => _NewHomePageState();
}
class _NewHomePageState extends State<NewHomePage> {
MoneroAccountListViewModel? accountListViewModel;
bool _lightningMode = false;
@override
void initState() {
super.initState();
_setAccountViewModel();
reaction((_)=>widget.dashboardViewModel.wallet, (_) {
_setAccountViewModel();
setState(() {
_lightningMode = false;
});
});
}
void _setAccountViewModel() {
accountListViewModel = widget.dashboardViewModel.balanceViewModel.hasAccounts
? getIt.get<MoneroAccountListViewModel>()
: null;
}
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surfaceDim,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Stack(
children: [
CustomScrollView(
physics: BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
slivers:[
SliverPadding(
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
sliver: CupertinoSliverRefreshControl(
refreshTriggerPullDistance: 160,
refreshIndicatorExtent: 90,
onRefresh: () => widget.dashboardViewModel.refreshDashboard(),
),
),
SliverToBoxAdapter(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
spacing: 24.0,
children: [
TopBar(
dashboardViewModel: widget.dashboardViewModel,
lightningMode: _lightningMode,
onLightningSwitchPress: () {
setState(() {
_lightningMode = !_lightningMode;
});
},
onSettingsButtonPress: () {
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => FractionallySizedBox(
child: Material(
child: NewSettingsPage(
dashboardViewModel: widget.dashboardViewModel,
authService: getIt.get<AuthService>(),
))),
);
},
),
Observer(
builder: (_)=>WalletInfoBar(
lightningMode: _lightningMode,
hardwareWalletType: widget.dashboardViewModel.wallet.hardwareWalletType,
name: widget.dashboardViewModel.wallet.name,
hasCustomize: accountListViewModel != null,
onCustomizeButtonTap: openAccountCustomizer),
),
Column(
children: [
Observer(
builder: (_) => CardsView(
key: ValueKey(widget.dashboardViewModel.wallet.name),
onCustomizeTapped: openCardCustomizer,
dashboardViewModel: widget.dashboardViewModel,
accountListViewModel: accountListViewModel,
onCompactModeBackgroundCardsTapped: openAccountCustomizer,
lightningMode: _lightningMode,
),
),
UnconfirmedBalanceWidget(dashboardViewModel: widget.dashboardViewModel,),
],
),
Observer(
builder: (_) {
return Column(
children: [
CoinActionRow(
lightningMode: _lightningMode,
showSwap: widget.dashboardViewModel.isEnabledSwapAction,),
MwebAd(dashboardViewModel: widget.dashboardViewModel,),
],
);
},
),
Observer(
builder: (_) => AssetsHistorySection(
nftViewModel: widget.nftViewModel,
dashboardViewModel: widget.dashboardViewModel,
),
),
SizedBox(height: 80.0)
],
),
),]
),
Container(
height: (MediaQuery.of(context).padding.top),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: <Color>[
Theme.of(context).colorScheme.surface.withAlpha(5),
Theme.of(context).colorScheme.surface.withAlpha(25),
Theme.of(context).colorScheme.surface.withAlpha(50),
Theme.of(context).colorScheme.surface.withAlpha(100),
Theme.of(context).colorScheme.surface.withAlpha(150),
Theme.of(context).colorScheme.surface.withAlpha(175),
Theme.of(context).colorScheme.surface.withAlpha(200),
],
),
),
),
],
),
);
}
void openAccountCustomizer() async {
await CupertinoScaffold.showCupertinoModalBottomSheet(
barrierColor: Colors.black.withAlpha(60),
context: context,
builder: (context) {
return ModalNavigator(
parentContext: context,
heightMode: ModalHeightModes.fullScreen,
rootPage: Material(
child: AccountCustomizer(
accountListViewModel: accountListViewModel!,
accountEditOrCreateViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>(),
dashboardViewModel: widget.dashboardViewModel,
)),
);
},
);
widget.dashboardViewModel.loadCardDesigns();
}
void openCardCustomizer() async {
final bloc = getIt.get<CardCustomizerBloc>(
param1: _lightningMode,
param2: widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi);
await CupertinoScaffold.showCupertinoModalBottomSheet(
barrierColor: Colors.black.withAlpha(60),
context: context,
builder: (context) {
return ModalNavigator(
parentContext: context,
heightMode: ModalHeightModes.fullScreen,
rootPage: BlocProvider(
create: (context) => bloc,
child: Material(
child: CardCustomizer(
cryptoTitle: widget.dashboardViewModel.wallet.currency.fullName ??
widget.dashboardViewModel.wallet.currency.name,
cryptoName: widget.dashboardViewModel.wallet.currency.name,
)),
));
},
);
bloc.add(DesignSaved());
await bloc.stream.firstWhere((s) => s is CardCustomizerSaved);
widget.dashboardViewModel.loadCardDesigns();
}
}
@@ -0,0 +1,315 @@
import 'dart:math';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart';
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/themes/core/theme_store.dart';
import 'package:cw_core/generate_name.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
class LightningUsernamePage extends StatefulWidget {
LightningUsernamePage({super.key, required this.isSetup, required this.themeStore, required this.lightningUsernameBloc});
final bool isSetup;
final ThemeStore themeStore;
late final bool isLightMode = !(themeStore.currentTheme.isDark);
final LightningUsernameBloc lightningUsernameBloc;
@override
State<LightningUsernamePage> createState() => _LightningUsernamePageState();
}
class _LightningUsernamePageState extends State<LightningUsernamePage> {
late bool _editMode;
final TextEditingController _controller = TextEditingController();
@override
void initState() {
super.initState();
_editMode = widget.isSetup;
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => widget.lightningUsernameBloc,
child: BlocConsumer<LightningUsernameBloc, LightningUsernameState>(
listener: (context, state) {
if (state is LightningUsernameSaved) {
if (widget.isSetup) {
Navigator.of(context).popUntil((route) => route.isFirst);
} else {
Navigator.of(context).pop();
}
}
if (state is LightningUsernameInitial) {
if (!widget.isSetup) {
_controller.text = state.username;
}
}
},
builder: (context, state) {
return Material(
child: Container(
decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface),
child: SafeArea(
child: Column(
children: [
ModalTopBar(
title: "Lightning ${S.of(context).username}",
leadingIcon: Icon(Icons.arrow_back_ios_new),
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
child: Column(
spacing: 24,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: _editMode
? LightningUsernameEditor(
isSetup: widget.isSetup,
controller: _controller,
isLightMode: widget.isLightMode,
onRandomizeButtonTap: () {
randomizeUsername(context);
},
state: state,
)
: LightningUsernameInfo(
username: state.username,
isLightMode: widget.isLightMode,
),
)
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
spacing: 12,
children: [
if (widget.isSetup) ...[
Text(
S.of(context).lightning_username_setup_later,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
SizedBox(),
NewPrimaryButton(
onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
text: S.of(context).skip,
color: Theme.of(context).colorScheme.surfaceContainer,
textColor: Theme.of(context).colorScheme.primary),
],
NewPrimaryButton(
onPressed: () {
if (_editMode) {
context.read<LightningUsernameBloc>().add(RequestUsernameSave());
} else {
setState(() {
_editMode = true;
});
}
},
text: _editMode ? S.of(context).confirm : S.of(context).change_username,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
disabled: _editMode && (!(state is LightningUsernameReady)),
isLoading: (state is LightningUsernameSaving),
),
SizedBox()
],
),
)
],
),
),
),
);
},
),
);
}
void randomizeUsername(BuildContext context) async {
final randomNumber = Random.secure().nextInt(9999);
final randomName = await generateName();
final username = "${randomName.replaceAll(" ", "")}$randomNumber".toLowerCase();
context.read<LightningUsernameBloc>().add(ChangeUsername(username));
_controller.text = username;
}
}
class LightningUsernameInfo extends StatelessWidget {
const LightningUsernameInfo({super.key, required this.username, required this.isLightMode});
final String username;
final bool isLightMode;
@override
Widget build(BuildContext context) {
return Column(
spacing: 24,
children: [
SvgPicture.asset(isLightMode
? "assets/new-ui/lightning_username_setup_light.svg"
: "assets/new-ui/lightning_username_setup.svg"),
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 2,
children: [
Text(
username,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
Text(
usernameSuffix,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary),
textAlign: TextAlign.center,
)
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Text(
S.of(context).lightning_username_desc_completed,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
),
)
],
);
}
}
class LightningUsernameEditor extends StatelessWidget {
const LightningUsernameEditor(
{super.key,
required this.controller,
required this.onRandomizeButtonTap,
required this.state,
required this.isSetup,
required this.isLightMode});
final TextEditingController controller;
final VoidCallback onRandomizeButtonTap;
final bool isLightMode;
final bool isSetup;
final LightningUsernameState state;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: [
SvgPicture.asset(isLightMode
? "assets/new-ui/lightning_username_setup_light.svg"
: "assets/new-ui/lightning_username_setup.svg"),
if (isSetup)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Text(
S.of(context).lightning_username_desc,
textAlign: TextAlign.center,
),
),
SizedBox(),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.horizontal(left: Radius.circular(16))),
child: Row(
spacing: 8,
children: [
Expanded(
child: TextField(
onChanged: (val) {
context.read<LightningUsernameBloc>().add(ChangeUsername(val));
},
controller: controller,
)),
Material(
color: Colors.transparent,
child: InkWell(
onTap: onRandomizeButtonTap,
child: SvgPicture.asset("assets/new-ui/randomize.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn)),
),
),
SizedBox()
],
),
),
),
Container(
alignment: Alignment.center,
height: 56,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.horizontal(right: Radius.circular(16))),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
usernameSuffix,
style: TextStyle(fontSize: 16),
),
),
)
],
),
),
if (state is LightningUsernameError)
Text(
(state as LightningUsernameError).error.message,
textAlign: TextAlign.center,
style: TextStyle(
color: (state as LightningUsernameError).error.isInfo
? Colors.green
: Theme.of(context).colorScheme.error),
)
else if (state is LightningUsernameReady)
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Icon(
Icons.check,
color: Colors.green,
size: 12,
),
Text(
S.of(context).username_available,
style: TextStyle(color: Colors.green),
)
],
)
else
Text(""),
SizedBox(height: MediaQuery.of(context).viewInsets.bottom.clamp(0, 100))
],
);
}
}
+346
View File
@@ -0,0 +1,346 @@
import 'package:cake_wallet/core/utilities.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_type.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_widget.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_display.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_info_box.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_widget.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_token_display.dart';
import 'package:cake_wallet/utils/share_util.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cake_wallet/zcash/zcash.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/payment_uris.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/routes.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
class NewReceivePage extends StatefulWidget {
NewReceivePage(
{super.key,
required this.addressListViewModel,
required this.receiveOptionViewModel,
required this.dashboardViewModel,
required this.lightningMode,
CryptoCurrency? initialCurrency}) {
if (initialCurrency != null && initialCurrency != addressListViewModel.selectedCurrency) {
addressListViewModel.setTokenCurrency(initialCurrency);
}
}
final WalletAddressListViewModel addressListViewModel;
final ReceiveOptionViewModel receiveOptionViewModel;
final DashboardViewModel dashboardViewModel;
final bool lightningMode;
@override
State<NewReceivePage> createState() => _NewReceivePageState();
}
class _NewReceivePageState extends State<NewReceivePage> {
bool _largeQrMode = false;
late WalletAddressListItem? _addressItemWithLabel;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.lightningMode) {
widget.receiveOptionViewModel.selectReceiveOption(
widget.receiveOptionViewModel.options
.firstWhereOrNull((item) => item.value.contains("Lightning")) ??
ReceivePageOption.mainnet,
);
widget.addressListViewModel.setTokenCurrency(CryptoCurrency.btcln);
} else if (widget.addressListViewModel.wallet.type == WalletType.bitcoin) {
widget.receiveOptionViewModel.selectReceiveOption(
widget.receiveOptionViewModel.options
.firstWhereOrNull((item) => item.value.contains("Standard")) ??
ReceivePageOption.mainnet,
);
}
});
reaction((_) => widget.addressListViewModel.uri, (newAddress) {
_reloadAddressWithLabel(newAddress);
});
_addressItemWithLabel =
widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull((item) {
return (item is WalletAddressListItem &&
item.address == widget.addressListViewModel.uri.address);
}) as WalletAddressListItem?;
reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption,
(ReceivePageOption option) {
if (widget.dashboardViewModel.type == WalletType.bitcoin &&
bitcoin!.isBitcoinReceivePageOption(option)) {
widget.addressListViewModel.setAddressType(bitcoin!.getOptionToType(option));
return;
}
if (widget.dashboardViewModel.type == WalletType.zcash) {
widget.addressListViewModel.setAddressType(zcash!.getOptionToType(option));
return;
}
switch (option) {
case ReceivePageOption.anonPayInvoice:
Navigator.pushNamed(
context,
Routes.anonPayInvoicePage,
arguments: [widget.addressListViewModel.address.address, option],
);
break;
case ReceivePageOption.anonPayDonationLink:
final sharedPreferences = getIt.get<SharedPreferences>();
final clearnetUrl = sharedPreferences.getString(PreferencesKey.clearnetDonationLink);
final onionUrl = sharedPreferences.getString(PreferencesKey.onionDonationLink);
final donationWalletName =
sharedPreferences.getString(PreferencesKey.donationLinkWalletName);
if (clearnetUrl != null &&
onionUrl != null &&
widget.addressListViewModel.wallet.name == donationWalletName) {
Navigator.pushNamed(
context,
Routes.anonPayReceivePage,
arguments: AnonPayReceivePageArgs(
invoiceInfo: AnonpayDonationLinkInfo(
clearnetUrl: clearnetUrl,
onionUrl: onionUrl,
address: widget.addressListViewModel.address.address,
),
qrImage: widget.addressListViewModel.qrImage,
),
);
} else {
Navigator.pushNamed(
context,
Routes.anonPayInvoicePage,
arguments: [widget.addressListViewModel.address.address, option],
);
}
break;
default:
if ([WalletType.bitcoin, WalletType.litecoin]
.contains(widget.addressListViewModel.type)) {
widget.addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option));
}
if (widget.addressListViewModel.type == WalletType.zcash) {
printV("help me i'll kms if that wont work: ${zcash!.getZcashAddressType(option)}");
widget.addressListViewModel.setAddressType(zcash!.getZcashAddressType(option));
}
}
});
}
@override
Widget build(BuildContext context) {
final hasAddressTypeSelector = widget.receiveOptionViewModel.options.length > 1;
final hasLabel = _addressItemWithLabel?.name != null && _addressItemWithLabel!.name!.isNotEmpty;
final infoboxDismissed = widget.addressListViewModel.wallet.walletInfo.receiveInfoboxDismissed;
final infobox = ReceiveInfoBox.forWalletType(widget.addressListViewModel.type,
onDismissed: () {
widget.addressListViewModel.dismissInfobox();
setState(() {});
}, autoGenerateSubaddressStatus: widget.lightningMode ? AutoGenerateSubaddressStatus.disabled : widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus);
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.surfaceBright,
Theme.of(context).colorScheme.surface,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.vertical(
top: Radius.circular(24),
),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
ModalTopBar(
title: _largeQrMode ? "" : S.of(context).receive,
leadingIcon: Icon(Icons.close),
trailingIcon: _largeQrMode
? Icon(Icons.share)
: widget.addressListViewModel.hasAddressRotation
/* TODO rotating is broken on mweb, disabling for now, fix after mvp*/
&&
!(widget.receiveOptionViewModel.selectedReceiveOption.description ?? "")
.toLowerCase()
.contains("mweb")
? Icon(Icons.refresh)
: null,
onLeadingPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
onTrailingPressed: () {
if (_largeQrMode) {
ShareUtil.share(
text: widget.addressListViewModel.uri.toString(),
context: context,
);
} else if (widget.addressListViewModel.hasAddressRotation) {
widget.addressListViewModel.rotateAddress();
}
},
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.max,
children: [
ReceiveAmountDisplay(
walletAddressListViewModel: widget.addressListViewModel,
largeQrMode: _largeQrMode,
),
ReceiveQrCode(
addressListViewModel: widget.addressListViewModel,
onTap: () {
setState(() {
_largeQrMode = !_largeQrMode;
// _infoboxDimissed = true;
widget.addressListViewModel.dismissInfobox();
});
},
largeQrMode: _largeQrMode,
),
if (widget.addressListViewModel.tokenCurrency != null)
ReceiveTokenDisplay(addressListViewModel: widget.addressListViewModel),
if (hasAddressTypeSelector)
ReceiveAddressTypeDisplay(
lightningMode: widget.lightningMode,
receiveOptionViewModel: widget.receiveOptionViewModel,
largeQrMode: _largeQrMode,
),
ReceiveAddressWidget(
addressListViewModel: widget.addressListViewModel,
),
GestureDetector(
onTap: _showLabelModal,
child: ReceiveLabelWidget(
name: _addressItemWithLabel?.name ?? "",
largeQrMode: _largeQrMode,
)),
Observer(
builder: (_) => ReceiveBottomButtons(
key: const ValueKey(0),
largeQrMode: _largeQrMode,
showAccountsButton: widget.addressListViewModel.hasAddressList,
showLabelButton: widget.addressListViewModel.hasAddressList && !hasLabel,
onCopyButtonPressed: () {
if (widget.addressListViewModel.hasPayjoin) {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) =>
PayjoinCopyModal(uri: widget.addressListViewModel.uri));
} else {
Clipboard.setData(
ClipboardData(
text: widget.addressListViewModel.displayAmount.isEmpty
? widget.addressListViewModel.uri.address
: widget.addressListViewModel.uri.toString()),
);
}
},
onAmountButtonPressed: () {
showMaterialModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withAlpha(80),
builder: (context) {
return ReceiveAmountModal(
walletAddressListViewModel: widget.addressListViewModel,
onSubmitted: (amount) {},
);
},
);
},
onLabelButtonPressed: _showLabelModal,
onAccountsButtonPressed: () {
Navigator.of(context).pushNamed(
Routes.receiveAddresses,
arguments: false,
);
},
),
),
ReceiveLargeAmountPreview(
amount: widget.addressListViewModel.displayAmount,
currency: widget.addressListViewModel.cryptoCurrencySymbol,
largeQrMode: _largeQrMode),
if (infobox != null && !widget.addressListViewModel.isLightning)
ClipRect(
child: AnimatedAlign(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
heightFactor: infoboxDismissed ? 0 : 1,
alignment: Alignment.center,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: infoboxDismissed ? 0 : 1,
curve: Curves.easeOutCubic,
child: infobox),
))
],
),
),
],
),
),
);
}
void _showLabelModal() {
showMaterialModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withAlpha(80),
builder: (context) {
return getIt.get<ReceiveLabelModal>(param1: _addressItemWithLabel);
}).then((value) {
_reloadAddressWithLabel(widget.addressListViewModel.uri);
});
}
void _reloadAddressWithLabel(PaymentURI newAddress) {
// FIXME: viewmodel doesn't want to load address name here, so we make it. investigate why later
setState(() {
_addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull(
(item) => (item is WalletAddressListItem && item.address == newAddress.address))
as WalletAddressListItem;
});
}
}
+10
View File
@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class ScanPage extends StatelessWidget {
const ScanPage({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
File diff suppressed because it is too large Load Diff
+147
View File
@@ -0,0 +1,147 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/modal_navigator.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import "package:cw_core/wallet_type.dart";
bool _trueFunc(DashboardViewModel _) => true;
bool _falseFunc(DashboardViewModel _) => false;
bool _isBtc(DashboardViewModel vm) => vm.wallet.type == WalletType.bitcoin;
bool _hasMweb(DashboardViewModel vm) => vm.hasMweb;
bool _isCupcake(DashboardViewModel vm) => vm.wallet.hardwareWalletType == HardwareWalletType.cupcake;
class SettingsListItem {
final String iconPath;
final String title;
final String route;
final Object? routeArgs;
final bool requireAuth;
final bool Function(DashboardViewModel) use2fa;
final bool Function(DashboardViewModel) condition;
const SettingsListItem(this.iconPath, this.title, this.route, {this.routeArgs = null, this.requireAuth = false, this.use2fa = _falseFunc, this.condition = _trueFunc});
}
class SettingsSectionData {
final String title;
final String titleIconPath;
final List<SettingsListItem> items;
const SettingsSectionData(this.title, this.titleIconPath, this.items);
static SettingsSectionData walletSettings =
SettingsSectionData(S.current.wallet_settings, "assets/new-ui/wallet_settings.svg", [
SettingsListItem("assets/new-ui/settings_row_icons/nodes.svg", S.current.nodes, Routes.manageNodes),
SettingsListItem("assets/new-ui/settings_row_icons/privacy.svg", S.current.privacy, Routes.privacyPage),
SettingsListItem("assets/new-ui/settings_row_icons/seed.svg", S.current.seed_and_keys, Routes.showKeys,
routeArgs: true, requireAuth: true, use2fa: (vm)=>vm.settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings),
SettingsListItem("assets/new-ui/settings_row_icons/lightning_username.svg",
"Lightning ${S.current.username}", Routes.lightningUsernamePage, condition: _isBtc),
SettingsListItem("assets/new-ui/settings_row_icons/silent-payments.svg", S.current.silent_payments_settings, Routes.silentPaymentsSettings, condition: _isBtc),
SettingsListItem("assets/new-ui/settings_row_icons/mweb.svg", S.current.litecoin_mweb_settings, Routes.mwebSettings, condition: _hasMweb),
SettingsListItem("assets/new-ui/settings_row_icons/cupcake.svg", S.current.export_outputs, Routes.urqrAnimatedPage, routeArgs: {'export-outputs': 'export-outputs'}, condition: _isCupcake),
SettingsListItem("assets/new-ui/settings_row_icons/other.svg", S.current.other, Routes.otherSettingsPage),
]);
static SettingsSectionData appSettings =
SettingsSectionData(S.current.app_settings, "assets/new-ui/app_settings.svg", [
SettingsListItem("assets/new-ui/settings_row_icons/connections.svg", S.current.connections, Routes.connectionSync),
// SettingsListItem("assets/new-ui/settings_row_icons/defaults.svg", "Defaults", ""),
SettingsListItem("assets/new-ui/settings_row_icons/display.svg", S.current.display, Routes.displaySettingsPage),
SettingsListItem("assets/new-ui/settings_row_icons/security.svg", S.current.security, Routes.securityBackupPage),
SettingsListItem("assets/new-ui/settings_row_icons/backup.svg", S.current.backup, Routes.backup, requireAuth: true, use2fa: (vm)=>vm.settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings),
]);
static SettingsSectionData otherSettings = SettingsSectionData("", "", [
SettingsListItem("assets/new-ui/settings_row_icons/support.svg", S.current.settings_support, Routes.support),
SettingsListItem("assets/new-ui/settings_row_icons/info.svg", S.current.about, Routes.aboutPage),
]);
static List<SettingsSectionData> all = [walletSettings, appSettings, otherSettings];
}
class NewSettingsPage extends StatelessWidget {
const NewSettingsPage({super.key, required this.dashboardViewModel, required this.authService});
final DashboardViewModel dashboardViewModel;
final AuthService authService;
@override
Widget build(BuildContext context) {
return ModalNavigator(
parentContext: context,
rootPage: SettingsMainPage(
dashboardViewModel: dashboardViewModel,
authService: authService,
));
}
}
class SettingsMainPage extends StatelessWidget {
const SettingsMainPage({super.key, required this.dashboardViewModel, required this.authService});
final DashboardViewModel dashboardViewModel;
final AuthService authService;
@override
Widget build(BuildContext context) {
Map<String, List<ListItem>> sections =
Map.fromEntries(SettingsSectionData.all.map((section) => MapEntry(
section.title,
section.items
.map((item) => item.condition(dashboardViewModel)
? ListItemRegularRow(
keyValue: item.title,
label: item.title,
iconPath: item.iconPath,
onTap: () {
if (item.route.isNotEmpty) {
if(item.requireAuth) {
authService.authenticateAction(context,
conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel),
route: item.route);
} else {
Navigator.of(context).pushNamed(item.route, arguments: item.routeArgs);
}
}
})
: null)
.whereType<ListItem>()
.toList())));
return Container(
color: Theme.of(context).colorScheme.surface,
child: Column(children: [
ModalTopBar(
title: "Settings",
leadingIcon: Icon(Icons.close),
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
onTrailingPressed: () {},
),
Expanded(
child: ListView(
controller: ModalScrollController.of(context),
children: [Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0),
child: NewListSections(
sections: sections,
),
),]
),
),
]),
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,168 @@
import 'package:bloc/bloc.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import "package:cw_core/balance_card_style_settings.dart";
import 'package:cw_core/card_design.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import "package:cw_core/wallet_type.dart";
import 'package:flutter/src/painting/gradient.dart';
import 'package:meta/meta.dart';
part 'card_customizer_event.dart';
part 'card_customizer_state.dart';
class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState> {
final WalletBase _wallet;
final bool lightningMode;
final bool displaySats;
CardCustomizerBloc(this._wallet, {this.lightningMode = false, this.displaySats = false})
: super(CardCustomizerNotLoaded(0, 0, [CardDesign.genericDefault], [], "", -1, displaySats, 0)) {
on<_Init>(_init);
on<CardDesignSelected>(_onDesignSelected);
on<ColorSelected>(_onColorSelected);
on<DesignSaved>(_onDesignSaved);
on<AccountNameChanged>(_onAccountNameChanged);
add(_Init());
}
List<Gradient> _updateAvailableColors(CardDesign currentDesign) {
final list = List<Gradient>.from(CardDesign.allGradients, growable: true);
if (CardDesign.specialDesignsForCurrencies[_wallet.currency] != null) {
list.add(CardDesign.specialDesignsForCurrencies[_wallet.currency]!.gradient);
}
return list;
}
Future<BalanceCardStyleSettings?> _loadCurrentDesignSettings(int accountIndex) async {
return (await BalanceCardStyleSettings.get(_wallet.walletInfo.internalId, accountIndex));
}
List<CardDesign> _initAvailableDesigns({bool lightningMode = false}) {
final List<CardDesign> ret = List<CardDesign>.empty(growable: true);
final curr = lightningMode ? CryptoCurrency.btcln : _wallet.currency;
ret.add(CardDesign.forCurrencyIcon(curr));
if (CardDesign.specialDesignsForCurrencies[curr] != null)
ret.add(CardDesign.forCurrencySpecial(curr));
return ret;
}
int _initSelectedDesign(CardDesign currentDesign) {
if (currentDesign.backgroundType == CardDesignBackgroundTypes.svgIcon)
return 0;
else if (currentDesign.backgroundType == CardDesignBackgroundTypes.svgFull)
return 1;
else
return 0;
}
int _initSelectedColor(CardDesign currentDesign) {
int ret = CardDesign.allGradients.indexOf(currentDesign.gradient);
if(ret == -1) {
// special design with its own color. select last color in list.
return CardDesign.allGradients.length;
} else if (ret == -1) {
// no color selected, select default.
return 0;
} else {
// select whatever's selected.
return ret;
}
}
void _init(_Init event, Emitter<CardCustomizerState> emit) async {
late final account;
if (_wallet.type == WalletType.monero) {
account = monero!.getCurrentAccount(_wallet);
} else if (_wallet.type == WalletType.wownero) {
account = wownero!.getCurrentAccount(_wallet);
} else {
account = null;
}
final accountName = (account?.label ?? "") as String;
late final int accountIndex;
if(account != null) {
accountIndex = account.id as int;
} else if(lightningMode) {
accountIndex = 0;
} else {
accountIndex = -1;
}
final currentDesignSettings = await _loadCurrentDesignSettings(accountIndex);
final currentDesign = CardDesign.fromStyleSettings(currentDesignSettings, lightningMode ? CryptoCurrency.btcln : _wallet.currency);
final availableDesigns = _initAvailableDesigns(lightningMode: lightningMode);
final availableColors = _updateAvailableColors(currentDesign);
final selectedDesign = _initSelectedDesign(currentDesign);
final selectedColor = _initSelectedColor(currentDesign);
emit(CardCustomizerInitial(selectedDesign, selectedColor, availableDesigns, availableColors,
accountName, accountIndex, displaySats, currentDesignSettings?.cardOrder ?? 0));
}
void _onDesignSelected(CardDesignSelected event, Emitter<CardCustomizerState> emit) {
final newColors = _updateAvailableColors(state.availableDesigns[event.newDesignIndex]);
late final int newColorIndex;
if (newColors.length < state.availableColors.length) {
newColorIndex = 0;
} else {
newColorIndex = state.selectedColorIndex;
}
emit(state.copyWith(
selectedDesignIndex: event.newDesignIndex,
availableColors: newColors,
selectedColorIndex: newColorIndex));
}
void _onColorSelected(ColorSelected event, Emitter<CardCustomizerState> emit) {
emit(state.copyWith(selectedColorIndex: event.newColorIndex));
}
void _onAccountNameChanged(AccountNameChanged event, Emitter<CardCustomizerState> emit) {
emit(state.copyWith(accountName: event.newAccountName));
}
void _onDesignSaved(DesignSaved event, Emitter<CardCustomizerState> emit) {
BalanceCardStyleSettings.fromCardDesign(
_wallet.walletInfo.internalId, state.accountIndex, state.cardOrder, state.selectedDesign)
.insert()
.then((value) {
emit(CardCustomizerSaved(state.selectedDesignIndex, state.selectedColorIndex,
state.availableDesigns, state.availableColors, state.accountName, state.accountIndex, state.displaySats, state.cardOrder));
});
saveAccountName();
}
Future<void> saveAccountName() async {
if (_wallet.type == WalletType.monero) {
await saveMoneroAccountName();
}
if (_wallet.type == WalletType.wownero) {
await saveWowneroAccountName();
}
}
Future<void> saveMoneroAccountName() async {
final MoneroAccountList moneroAccountList = monero!.getAccountList(_wallet);
await moneroAccountList.setLabelAccount(_wallet,
accountIndex: state.accountIndex, label: state.accountName);
await _wallet.save();
}
Future<void> saveWowneroAccountName() async {
final WowneroAccountList wowneroAccountList = wownero!.getAccountList(_wallet);
await wowneroAccountList.setLabelAccount(_wallet,
accountIndex: state.accountIndex, label: state.accountName);
await _wallet.save();
}
}
@@ -0,0 +1,28 @@
part of 'card_customizer_bloc.dart';
@immutable
sealed class CardCustomizerEvent {}
class _Init extends CardCustomizerEvent {}
class CardDesignSelected extends CardCustomizerEvent {
final int newDesignIndex;
CardDesignSelected(this.newDesignIndex);
}
class ColorSelected extends CardCustomizerEvent {
final int newColorIndex;
ColorSelected(this.newColorIndex);
}
class AccountNameChanged extends CardCustomizerEvent {
final String newAccountName;
AccountNameChanged(this.newAccountName);
}
class DesignSaved extends CardCustomizerEvent {}
@@ -0,0 +1,112 @@
part of 'card_customizer_bloc.dart';
sealed class CardCustomizerState {
final int selectedDesignIndex;
final int selectedColorIndex;
final String accountName;
final int accountIndex;
final int cardOrder;
final bool displaySats;
final List<CardDesign> availableDesigns;
final List<Gradient> availableColors;
CardCustomizerState(
this.selectedDesignIndex,
this.selectedColorIndex,
this.availableDesigns,
this.availableColors,
this.accountName,
this.accountIndex,
this.displaySats,
this.cardOrder
);
CardDesign get selectedDesign {
return availableDesigns[selectedDesignIndex].withGradient(selectedColor);
}
CardCustomizerState copyWith({
int? selectedDesignIndex,
int? selectedColorIndex,
List<CardDesign>? availableDesigns,
List<Gradient>? availableColors,
String? accountName,
int? accountIndex,
int? cardOrder,
});
Gradient get selectedColor => availableColors[selectedColorIndex];
}
final class CardCustomizerNotLoaded extends CardCustomizerState {
CardCustomizerNotLoaded(super.selectedDesignIndex, super.selectedColorIndex, super.availableDesigns, super.availableColors, super.accountName, super.accountIndex, super.displaySats, super.cardOrder);
@override
CardCustomizerState copyWith({int? selectedDesignIndex, int? selectedColorIndex, List<CardDesign>? availableDesigns, List<Gradient>? availableColors, String? accountName, int? accountIndex, int? cardOrder}) {
// this is never gonna be copied. it's near-instantly replaced with initial
throw UnimplementedError();
}
}
final class CardCustomizerInitial extends CardCustomizerState {
CardCustomizerInitial(
int selectedDesignIndex,
int selectedColorIndex,
List<CardDesign> availableDesigns,
List<Gradient> availableColors,
String accountName,
int accountIndex,
bool displaySats,
int cardOrder,
) : super(selectedDesignIndex, selectedColorIndex, availableDesigns, availableColors, accountName,
accountIndex, displaySats, cardOrder);
CardCustomizerInitial copyWith({
int? selectedDesignIndex,
int? selectedColorIndex,
List<CardDesign>? availableDesigns,
List<Gradient>? availableColors,
String? accountName,
int? accountIndex,
bool? displaySats,
int? cardOrder,
}) {
return CardCustomizerInitial(
selectedDesignIndex ?? this.selectedDesignIndex,
selectedColorIndex ?? this.selectedColorIndex,
availableDesigns ?? this.availableDesigns,
availableColors ?? this.availableColors,
accountName ?? this.accountName,
accountIndex ?? this.accountIndex,
displaySats ?? this.displaySats,
cardOrder ?? this.cardOrder,
);
}
}
final class CardCustomizerSaved extends CardCustomizerState {
CardCustomizerSaved(super.selectedDesignIndex, super.selectedColorIndex, super.availableDesigns,
super.availableColors, super.accountName, super.accountIndex, super.displaySats, super.cardOrder);
@override
CardCustomizerState copyWith(
{int? selectedDesignIndex,
int? selectedColorIndex,
List<CardDesign>? availableDesigns,
List<Gradient>? availableColors,
String? accountName,
int? accountIndex,
bool? displaySats,
int? cardOrder}) {
return CardCustomizerSaved(
selectedDesignIndex ?? this.selectedDesignIndex,
selectedColorIndex ?? this.selectedColorIndex,
availableDesigns ?? this.availableDesigns,
availableColors ?? this.availableColors,
accountName ?? this.accountName,
accountIndex ?? this.accountIndex,
displaySats ?? this.displaySats,
cardOrder ?? this.cardOrder,
);
}
}
@@ -0,0 +1,92 @@
import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/lnurlpay_record.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:meta/meta.dart';
part 'lightning_username_event.dart';
part 'lightning_username_state.dart';
class UsernameError {
String message;
bool isInfo;
UsernameError(this.message, {this.isInfo = false});
}
// TODO find other errors that api returns
final Map<String, String> errorPatterns = {"name already taken": S.current.username_not_available};
const usernameRegex = r"^[a-z0-9_.-]+$";
const usernameSuffix = "@cake.cash";
class LightningUsernameBloc extends Bloc<LightningUsernameEvent, LightningUsernameState> {
final WalletBase _wallet;
LightningUsernameBloc(this._wallet) : super(LightningUsernameInitial("")) {
on<ChangeUsername>(onUsernameChanged, transformer: restartable());
on<RequestUsernameSave>(onUsernameSaveRequested);
on<_Init>((event, emit) async {
final username = await bitcoin!.getLightningUsername(_wallet);
emit(LightningUsernameInitial(username ?? ""));
});
add(_Init());
}
Future<void> onUsernameChanged(ChangeUsername event, Emitter<LightningUsernameState> emit) async {
emit(LightningUsernameChecking(event.newUsername));
final error = await getUsernameError(event.newUsername);
if (error != null) {
emit(LightningUsernameError(event.newUsername, error));
} else {
emit(LightningUsernameReady(event.newUsername));
}
}
Future<void> onUsernameSaveRequested(RequestUsernameSave event, Emitter<LightningUsernameState> emit) async {
emit(LightningUsernameSaving(state.username));
try {
await bitcoin!.setLightningUsername(_wallet, state.username);
emit(LightningUsernameSaved(state.username));
} catch (e) {
for (final pattern in errorPatterns.keys) {
if (e.toString().contains(pattern)) {
emit(LightningUsernameError(state.username, UsernameError(errorPatterns[pattern]!)));
return;
}
}
emit(LightningUsernameError(state.username, UsernameError(bitcoin!.getBreezSdkError(e) ?? e.toString())));
}
}
Future<UsernameError?> getUsernameError(String username) async {
if (!(RegExp(usernameRegex).hasMatch(username))) {
return UsernameError("${S.current.username_character_error} a-z, 0-9, - _ .");
}
if (username.length < 1 || username.length > 64) {
return UsernameError(S.current.username_length_error_1_64);
}
if (username == (await bitcoin!.getLightningUsername(_wallet))) {
return UsernameError(S.current.already_your_username, isInfo: true);
}
if ((await LNUrlPayRecord.checkWellKnownUsername(
"${username}${usernameSuffix}", CryptoCurrency.btc)) !=
null) {
return UsernameError(S.current.username_not_available);
}
return null;
}
}
@@ -0,0 +1,14 @@
part of 'lightning_username_bloc.dart';
@immutable
sealed class LightningUsernameEvent {}
final class _Init extends LightningUsernameEvent {}
final class ChangeUsername extends LightningUsernameEvent {
final String newUsername;
ChangeUsername(this.newUsername);
}
final class RequestUsernameSave extends LightningUsernameEvent {}
@@ -0,0 +1,34 @@
part of 'lightning_username_bloc.dart';
@immutable
sealed class LightningUsernameState {
final String username;
LightningUsernameState(this.username);
}
final class LightningUsernameInitial extends LightningUsernameState {
LightningUsernameInitial(super.username);
}
final class LightningUsernameChecking extends LightningUsernameState {
LightningUsernameChecking(super.username);
}
final class LightningUsernameError extends LightningUsernameState {
final UsernameError error;
LightningUsernameError(super.username, this.error);
}
final class LightningUsernameReady extends LightningUsernameState {
LightningUsernameReady(super.username);
}
final class LightningUsernameSaving extends LightningUsernameState {
LightningUsernameSaving(super.username);
}
final class LightningUsernameSaved extends LightningUsernameState {
LightningUsernameSaved(super.username);
}
@@ -0,0 +1,85 @@
import 'dart:ui';
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_text_field.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
class AddressLabelInputPopup extends StatefulWidget {
const AddressLabelInputPopup({
super.key,
required this.walletAddressEditOrCreateViewModel,
});
final WalletAddressEditOrCreateViewModel walletAddressEditOrCreateViewModel;
@override
State<AddressLabelInputPopup> createState() => _AddressLabelInputPopupState();
}
class _AddressLabelInputPopupState extends State<AddressLabelInputPopup> {
late final TextEditingController _controller;
late final FocusNode _focusNode;
@override
void initState() {
super.initState();
_controller = TextEditingController();
_focusNode = FocusNode();
reaction((_) => widget.walletAddressEditOrCreateViewModel.state,
(AddressEditOrCreateState state) {
if (state is AddressSavedSuccessfully) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted)
Navigator.of(context).pop(widget.walletAddressEditOrCreateViewModel.label);
});
}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
_controller.text = widget.walletAddressEditOrCreateViewModel.label;
});
}
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: NewListSections(
sections: {
"": [
ListItemTextField(
keyValue: "label",
label: S.of(context).label,
focusNode: _focusNode,
onFieldSubmitted: (value) {
widget.walletAddressEditOrCreateViewModel.label = value;
widget.walletAddressEditOrCreateViewModel.save();
// Navigator.of(context).pop();
})
]
},
controllers: {"label": _controller},
),
),
]),
);
}
}
+49
View File
@@ -0,0 +1,49 @@
import 'package:cake_wallet/new-ui/widgets/dropdown_row.dart';
import 'package:flutter/material.dart';
class AnimatedDropdown extends StatefulWidget {
const AnimatedDropdown({super.key,required this.content, required this.dropdownText});
final Widget content;
final String dropdownText;
@override
State<AnimatedDropdown> createState() => _AnimatedDropdownState();
}
class _AnimatedDropdownState extends State<AnimatedDropdown> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return ClipRSuperellipse(
borderRadius: BorderRadius.circular(20),
child: Container(
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(20),
),
color: Theme.of(context).colorScheme.surfaceContainer,),
child: Column(
children: [
DropdownRow(expanded: _expanded, text: widget.dropdownText,onTap: (){
setState(() {
_expanded = !_expanded;
});
},),
AnimatedOpacity(duration:Duration(milliseconds: 150),opacity:_expanded?1:0,child: Divider(height: 1,thickness: 1,)),
AnimatedCrossFade(
firstChild: Container(height:0),
secondChild: widget.content,
crossFadeState: _expanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 300),
sizeCurve: Curves.easeOutCubic,
)
],
),
),
);
}
}
+147
View File
@@ -0,0 +1,147 @@
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/themes/core/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class AppsWidget extends StatelessWidget {
AppsWidget({
this.onTap,
required this.title,
required this.subTitle,
this.hint,
this.svgPicture,
this.image,
this.icon,
this.onClose,
this.customBorder,
this.shadowSpread,
this.shadowBlur,
super.key,
this.marginV,
this.marginH,
this.isWide,
this.isLink,
this.isCake,
});
final VoidCallback? onTap;
final VoidCallback? onClose;
final String title;
final String subTitle;
final Widget? hint;
final SvgPicture? svgPicture;
final Widget? icon;
final String? image;
final double? customBorder;
final double? marginV;
final double? marginH;
final double? shadowSpread;
final double? shadowBlur;
final bool? isWide;
final bool? isLink;
final bool? isCake;
@override
Widget build(BuildContext context) {
if (isWide == true) {
return Stack(
children: [
Container(
margin: EdgeInsets.symmetric(horizontal: marginH ?? 20, vertical: marginV ?? 5),
width: double.infinity,
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(18)),
gradient: LinearGradient(
colors: [
context.customColors.cardGradientColorPrimary,
context.customColors.cardGradientColorSecondary,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: TextButton(
onPressed: onTap,
style: TextButton.styleFrom(
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(18),
side: BorderSide(
width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh),
),
padding: EdgeInsets.all(24),
),
child: Column(
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 20),
child: CakeImageWidget(imageUrl: image, height: 54, width: 54),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 6.0,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w800,
fontSize: 20,
),
softWrap: true,
),
isCake == true ? CakeImageWidget(imageUrl: "assets/new-ui/cakelabs-icon.svg", color: Theme.of(context).colorScheme.onSurfaceVariant) : SizedBox(),
],
),
SizedBox(height: 5),
Text(
subTitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
softWrap: true,
),
],
),
),
Icon(
isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios,
color: Theme.of(context).colorScheme.onSurfaceVariant,
size: 20,
)
],
),
if (hint != null) ...[
SizedBox(height: 10),
hint!,
]
],
),
),
),
if (onClose != null)
Positioned(
top: 10,
right: 10,
child: IconButton(
icon: Icon(Icons.close),
onPressed: onClose,
//color: Theme.of(context).colorScheme.onSurface,
),
),
],
);
}
else {
return Stack(
);
}
}
}
+202
View File
@@ -0,0 +1,202 @@
import 'dart:convert';
import 'dart:io';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:url_launcher/url_launcher.dart';
const changelogIconLocation = "assets/new-ui/changelog/icons";
const changelogTextLocation = "assets/new-ui/changelog/text";
class ChangelogItem {
final String iconFilename;
final String title;
final String description;
ChangelogItem(this.iconFilename, this.title, this.description);
}
class ChangelogModal extends StatefulWidget {
const ChangelogModal({super.key, required this.version});
final String version;
@override
State<ChangelogModal> createState() => _ChangelogModalState();
}
class _ChangelogModalState extends State<ChangelogModal> {
List<ChangelogItem> _items = [];
@override
void initState() {
super.initState();
loadChangelog();
}
void loadChangelog() async {
String lang = WidgetsBinding.instance.platformDispatcher.locale.languageCode;
if (lang == "und" ||
lang.isEmpty ||
!(await File("$changelogTextLocation/changelog_$lang.json").exists())) {
lang = "en";
}
final List<dynamic> changelog =
jsonDecode(await rootBundle.loadString("$changelogTextLocation/changelog_$lang.json"))
as List<dynamic>;
for (final item in changelog) {
final itemMap = item as Map<String, dynamic>;
_items.add(ChangelogItem(
itemMap["icon"] as String? ?? "",
itemMap["title"] as String? ?? "",
itemMap["description"] as String? ?? "",
));
}
setState(() {});
}
@override
Widget build(BuildContext context) {
if (_items.isEmpty) return SizedBox.shrink();
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: [
SizedBox(),
ModalTopBar(title: S.of(context).whats_new),
SizedBox(),
Flexible(
child: SingleChildScrollView(
child: Column(
children: [
VersionNumberHeader(version: widget.version),
SizedBox(height: 40),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 32,
children: _items.map((item) => ChangelogItemWidget(item: item)).toList(),
),
SizedBox(height: 32),
Text(
S.of(context).and_much_more_to_come,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
)
],
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
spacing: 12,
children: [
NewPrimaryButton(
onPressed: () {
try {
launchUrl(Uri.https("blog.cakewallet.com"));
} catch (_) {}
},
text: S.of(context).view_more_info,
color: Theme.of(context).colorScheme.surfaceContainer,
textColor: Theme.of(context).colorScheme.primary),
NewPrimaryButton(
onPressed: Navigator.of(context).pop,
text: S.of(context).continue_text,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary),
],
),
)
],
),
),
);
}
}
class VersionNumberHeader extends StatelessWidget {
VersionNumberHeader({super.key, required this.version});
final String version;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.primary, width: 2),
borderRadius: BorderRadius.circular(99999999)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16),
child: Row(
spacing: 8,
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
"assets/images/cake_logo_dark.svg",
height: 32,
width: 32,
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.onSurface, BlendMode.srcIn),
),
Text(
version,
style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontSize: 36),
)
],
),
),
);
}
}
class ChangelogItemWidget extends StatelessWidget {
const ChangelogItemWidget({super.key, required this.item});
final ChangelogItem item;
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: [
if (item.iconFilename.isNotEmpty)
SvgPicture.asset(
"$changelogIconLocation/${item.iconFilename}.svg",
height: 36,
width: 36,
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
if (item.title.isNotEmpty)
Text(
item.title,
style: TextStyle(fontWeight: FontWeight.w500),
),
if (item.description.isNotEmpty)
Text(item.description,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
))
],
)
],
);
}
}
@@ -0,0 +1,128 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class CoinControlListItem extends StatelessWidget {
CoinControlListItem({
required this.note,
required this.amount,
required this.fiatAmount,
required this.address,
required this.isSending,
required this.isFrozen,
required this.isChange,
required this.isSilentPayment,
required this.isFirst,
required this.isLast,
required this.isLoading,
this.onCheckBoxTap,
});
final String note;
final String amount;
final String fiatAmount;
final String address;
final bool isSending;
final bool isFrozen;
final bool isChange;
final bool isSilentPayment;
final bool isFirst;
final bool isLast;
final bool isLoading;
final Function()? onCheckBoxTap;
@override
Widget build(BuildContext context) {
return Container(
height:70,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(
top: isFirst ? Radius.circular(12) : Radius.circular(0),
bottom: isLast ? Radius.circular(12) : Radius.circular(0),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
spacing: 12,
children: [
_getLeading(context),
// NewSimpleCheckbox(value: isSending, onChanged: (value){onCheckBoxTap?.call();}),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4,
children: [
Row(
spacing:4,
children: [
Text(amount, style: TextStyle(fontSize:14,fontWeight: FontWeight.w400,color: Theme.of(context).colorScheme.onSurface),),
if(address.toLowerCase().contains("mweb")) // hack carried over from old ui, we really should just have a boolean in the object
SvgPicture.asset(
"assets/new-ui/address-type-picker-icons/mweb.svg",
width: 18,
height: 18,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
if(isSilentPayment)
SvgPicture.asset(
"assets/new-ui/address-type-picker-icons/silent.svg",
width: 18,
height: 18,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
],
),
AutoSizeText(
'${address.substring(0, 5)}...${address.substring(address.length - 5)}',
style: TextStyle(fontSize:12,fontWeight: FontWeight.w400,color: Theme.of(context).colorScheme.onSurfaceVariant),
maxLines: 1,
),
],
)
],
),
Row(
spacing: 8,
children: [
Text(
fiatAmount,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
SvgPicture.asset(
"assets/new-ui/arrow_right.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
],
)
],
),
)
);
}
Widget _getLeading(BuildContext context) {
if(isLoading) {
return CircularProgressIndicator(color: Theme.of(context).colorScheme.primary,);
}
if(isFrozen) {
return SvgPicture.asset("assets/new-ui/frozen.svg");
}
return NewSimpleCheckbox(value: isSending, onChanged: (value){onCheckBoxTap?.call();});
}
}
@@ -0,0 +1,70 @@
import 'dart:math';
import 'package:cake_wallet/themes/core/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
class CoinActionButton extends StatelessWidget {
const CoinActionButton({
super.key,
required this.icon,
required this.label,
required this.action,
});
final SvgPicture icon;
final String label;
final VoidCallback action;
static const sizeFactor = 0.16;
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size.width * sizeFactor;
final double effectiveSize = min(size, 80);
return Column(
children: [
Container(
width: effectiveSize,
height: effectiveSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
context.customColors.cardGradientColorPrimary,
context.customColors.cardGradientColorSecondary
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
border: Border.all(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
width: 1,
),
),
child: IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
HapticFeedback.mediumImpact();
action();
},
icon: icon,
color: Theme.of(context).colorScheme.primary,
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
label,
),
),
],
);
}
}
@@ -0,0 +1,204 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/modal_navigator.dart';
import 'package:cake_wallet/new-ui/pages/send_page.dart';
import 'package:cake_wallet/new-ui/pages/swap_page.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/lnurl.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import '../../../pages/receive_page.dart';
import '../../../pages/scan_page.dart';
import 'coin_action_button.dart';
class CoinActionRow extends StatelessWidget {
const CoinActionRow({super.key, this.lightningMode = false, this.showSwap = true});
final bool lightningMode;
final bool showSwap;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
spacing: MediaQuery.of(context).size.width * 0.05,
children: [
CoinActionButton(
icon: SvgPicture.asset(
"assets/new-ui/send.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
label: S.of(context).send,
action: () {
if (FeatureFlag.hasNewUiExtraPages) {
final sendPage = getIt.get<NewSendPage>(
param1: SendPageParams(
unspentCoinType:
lightningMode ? UnspentCoinType.lightning : UnspentCoinType.nonMweb,
),
);
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(60),
builder: (context) {
return Material(
child: ModalNavigator(
rootPage: sendPage,
parentContext: context,
),
);
},
);
} else {
Map<String, dynamic>? args;
if (lightningMode) args = {'coinTypeToSpendFrom' : UnspentCoinType.lightning};
Navigator.of(context).pushNamed(Routes.send, arguments: args);
}
},
),
CoinActionButton(
icon: SvgPicture.asset(
"assets/new-ui/receive.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
label: S.of(context).receive,
action: () async {
if (FeatureFlag.hasNewUiExtraPages) {
final page = getIt.get<NewReceivePage>(param1: lightningMode);
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(60),
builder: (context) {
return Material(child: ModalNavigator(parentContext:context,rootPage: page));
},
);
} else {
// ToDo: (Konsti) refactor as part of the derivation PR (I hate myself for it)
if (lightningMode) {
await getIt<WalletAddressListViewModel>().setAddressType(
bitcoin!.getOptionToType(bitcoin!.getBitcoinLightningReceivePageOption()));
} else {
await getIt<WalletAddressListViewModel>().setAddressType(
bitcoin!.getOptionToType(bitcoin!.getBitcoinSegwitPageOption()));
}
Navigator.of(context).pushNamed(Routes.addressPage);
}
},
),
if (showSwap)
CoinActionButton(
icon: SvgPicture.asset(
"assets/new-ui/exchange.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
label: S.of(context).swap,
action: () {
final page = getIt.get<NewSwapPage>(param2: lightningMode ? CryptoCurrency.btcln : null);
if (FeatureFlag.hasNewUiExtraPages) {
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => Material(
child: ModalNavigator(
rootPage: page,
parentContext: context,
)),
);
} else {
Navigator.of(context).pushNamed(Routes.exchange);
}
},
),
CoinActionButton(
icon: SvgPicture.asset(
"assets/new-ui/scan.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
label: S.of(context).scan,
action: () => _onPressedScan(context),
),
],
),
);
}
Future<void> _onPressedScan(BuildContext context) async {
if (false && FeatureFlag.hasNewUiExtraPages) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => FractionallySizedBox(
heightFactor: 0.9,
child: ScanPage(),
),
);
} else {
final code = await presentQRScanner(context);
if (code == null || code.isEmpty) return;
late final PaymentRequest req;
var unspentCoinType = UnspentCoinType.any;
if (SendViewModelBase.isNonZeroAmountLightningInvoice(code)) {
unspentCoinType = UnspentCoinType.lightning;
final amount = CryptoCurrency.btcln.formatAmount(BigInt.from(getBolt11Amount(code) ?? 0));
req = PaymentRequest(code, amount, "", "", "");
} else if (SendViewModelBase.isLnurlInvoice(code)) {
unspentCoinType = UnspentCoinType.lightning;
final amount = CryptoCurrency.btcln.formatAmount(BigInt.from(await LNURL.getPayRequestAmount(code) ?? 0));
req = PaymentRequest(code, amount, "", "", "");
} else if (OpenCryptoPayService.isOpenCryptoPayQR(code)) {
req = PaymentRequest(code, "", "", "", "");
} else {
final uri = Uri.tryParse(code);
if (uri == null) return;
req = PaymentRequest.fromUri(uri);
}
final sendPage = getIt.get<NewSendPage>(
param1: SendPageParams(
initialPaymentRequest: req,
unspentCoinType: unspentCoinType,
),
);
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(60),
builder: (context) => Material(
child: ModalNavigator(
rootPage: sendPage,
parentContext: context,
),
),
);
}
}
}
@@ -0,0 +1,37 @@
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile_base.dart';
import 'package:flutter/material.dart';
class AnonpayHistoryTile extends StatelessWidget {
const AnonpayHistoryTile(
{super.key,
required this.provider,
required this.createdAt,
required this.amount,
required this.currency,
required this.roundedTop,
required this.roundedBottom,
required this.bottomSeparator});
final String provider;
final String createdAt;
final String amount;
final String currency;
final bool roundedTop;
final bool roundedBottom;
final bool bottomSeparator;
@override
Widget build(BuildContext context) {
return HistoryTileBase(
title: provider,
date: createdAt,
amount: amount + " " + currency,
leadingIcon: ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset('assets/images/trocador.png', width: 36, height: 36)),
amountFiat: "",
roundedTop: roundedTop,
roundedBottom: roundedBottom,
bottomSeparator: bottomSeparator);
}
}
@@ -0,0 +1,342 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/modal_navigator.dart';
import 'package:cake_wallet/new-ui/pages/receive_page.dart';
import 'package:cake_wallet/new-ui/pages/send_page.dart';
import 'package:cake_wallet/new-ui/pages/swap_page.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
enum AssetDetailsModalModes { normal, ltcTransparent, ltcPrivate }
class AssetDetailsModal extends StatelessWidget {
const AssetDetailsModal(
{super.key,
required this.title,
required this.chainTitle,
required this.subtitle,
required this.amount,
required this.currencyTitle,
required this.fiatAmount,
required this.iconPath,
required this.chainIconPath,
required this.mode,
required this.wallet,
required this.showSwap,
this.asset});
final String title;
final CryptoCurrency? asset;
final String chainTitle;
final String subtitle;
final String amount;
final String currencyTitle;
final String fiatAmount;
final String iconPath;
final String chainIconPath;
final WalletBase wallet;
final bool showSwap;
final AssetDetailsModalModes mode;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ModalTopBar(
title: "",
trailingIcon: Icon(Icons.close),
onTrailingPressed: Navigator.of(context).pop,
),
SafeArea(
child: Column(
spacing: 24,
children: [
Column(
spacing: 15,
children: [
SizedBox(
width: 75,
height: 75,
child: Stack(
children: [
if (iconPath.isNotEmpty)
Image.asset(iconPath, width: 75, height: 75)
else
Container(
width: 75,
height: 75,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(99999)),
child: Center(
child: Text(
title.substring(0, 2),
style: TextStyle(
fontSize: 28, color: Theme.of(context).colorScheme.onPrimary),
)),
),
if (chainIconPath.isNotEmpty)
Align(
alignment: Alignment.bottomRight,
child: Container(
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.black)),
color: Colors.white),
child: Padding(
padding: const EdgeInsets.all(4.0),
child: SvgPicture.asset(
chainIconPath,
width: 18,
height: 18,
colorFilter:
ColorFilter.mode(Colors.black, BlendMode.srcIn),
),
)))
],
),
),
Column(
spacing: 4,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4,
children: [
Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface),
),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(999999999)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
child: Text(
chainTitle,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
)
],
),
if (subtitle.isNotEmpty)
Text(
subtitle,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
)
],
),
Container(
height: 1,
width: MediaQuery.of(context).size.width * 0.8,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
Column(
spacing: 4,
children: [
Row(
spacing: 4,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
amount,
style: TextStyle(fontSize: 28),
),
Text(currencyTitle,
style: TextStyle(
fontSize: 28,
color: Theme.of(context).colorScheme.onSurfaceVariant))
],
),
Text(
fiatAmount,
style: TextStyle(
fontSize: 20, color: Theme.of(context).colorScheme.onSurfaceVariant),
)
],
),
SizedBox(),
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 20,
children: [
if (mode == AssetDetailsModalModes.ltcTransparent)
AssetDetailsModalBottomButton(
iconPath: "assets/new-ui/mask.svg",
title: S.of(context).mask,
onPressed: () {
Navigator.of(context).pop();
depositToL2(context);
},
),
if (mode == AssetDetailsModalModes.ltcPrivate)
AssetDetailsModalBottomButton(
iconPath: "assets/new-ui/unmask.svg",
title: S.of(context).unmask,
onPressed: () {
Navigator.of(context).pop();
withdrawFromL2(context);
},
),
AssetDetailsModalBottomButton(
iconPath: "assets/new-ui/send.svg",
title: S.of(context).send,
onPressed: () => openPage<NewSendPage>(
context,
param1: SendPageParams(
initialCurrency: asset,
unspentCoinType: unspentCoinType,
),
),
),
AssetDetailsModalBottomButton(
iconPath: "assets/new-ui/receive.svg",
title: S.of(context).receive,
onPressed: () async {
if (mode == AssetDetailsModalModes.ltcPrivate) {
await bitcoin!.setAddressType(
wallet,
bitcoin!
.getOptionToType(bitcoin!.getLitecoinMwebReceivePageOption()));
}
openPage<NewReceivePage>(context, param2: asset);
},
),
if (showSwap && mode != AssetDetailsModalModes.ltcPrivate)
AssetDetailsModalBottomButton(
iconPath: "assets/new-ui/exchange.svg",
title: S.of(context).swap,
onPressed: () => openPage<NewSwapPage>(context, param2: asset),
),
],
),
SizedBox()
],
),
)
],
),
);
}
UnspentCoinType get unspentCoinType {
switch (mode) {
case AssetDetailsModalModes.ltcTransparent:
return UnspentCoinType.nonMweb;
case AssetDetailsModalModes.ltcPrivate:
return UnspentCoinType.mweb;
case AssetDetailsModalModes.normal:
return UnspentCoinType.any;
}
}
void openPage<T extends Object>(BuildContext context, {dynamic param1, dynamic param2}) {
final page = getIt.get<T>(param1: param1, param2: param2);
showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(60),
builder: (context) {
return Material(child: ModalNavigator(parentContext: context, rootPage: page as Widget));
},
);
}
Future<void> depositToL2(BuildContext context) async {
PaymentRequest? paymentRequest = null;
final depositAddress = bitcoin!.getUnusedMwebAddress(wallet);
if ((depositAddress?.isNotEmpty ?? false)) {
paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$depositAddress"));
}
final page = getIt.get<NewSendPage>(
param1: SendPageParams(
initialPaymentRequest: paymentRequest,
unspentCoinType: UnspentCoinType.nonMweb,
mode: SendPageModes.mwebDeposit,
));
showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(128),
builder: (context) {
return FractionallySizedBox(
heightFactor: 0.65,
child: ModalNavigator(parentContext: context, rootPage: Material(child: page)));
});
}
Future<void> withdrawFromL2(BuildContext context) async {
PaymentRequest? paymentRequest = null;
UnspentCoinType unspentCoinType = UnspentCoinType.any;
final withdrawAddress = bitcoin!.getUnusedSegwitAddress(wallet);
if ((withdrawAddress?.isNotEmpty ?? false)) {
paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$withdrawAddress"));
}
unspentCoinType = UnspentCoinType.mweb;
final page = getIt.get<NewSendPage>(
param1: SendPageParams(
initialPaymentRequest: paymentRequest,
unspentCoinType: unspentCoinType,
mode: SendPageModes.mwebWithdrawal,
));
showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(128),
builder: (context) {
return FractionallySizedBox(
heightFactor: 0.65,
child: ModalNavigator(parentContext: context, rootPage: Material(child: page)));
});
}
}
class AssetDetailsModalBottomButton extends StatelessWidget {
const AssetDetailsModalBottomButton(
{super.key, required this.iconPath, required this.title, required this.onPressed});
final String iconPath;
final String title;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Column(
spacing: 8,
children: [
ModernButton.svg(
onPressed: onPressed,
svgPath: iconPath,
size: 60,
iconSize: 32,
),
Text(title)
],
);
}
}
@@ -0,0 +1,197 @@
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart';
import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class AssetTile extends StatelessWidget {
const AssetTile(
{super.key,
required this.balance,
required this.chainIconPath,
this.showSecondary = false,
this.title,
this.trailingText,
this.modalMode = AssetDetailsModalModes.normal,
required this.wallet, required this.showSwap});
final BalanceRecord balance;
final bool showSecondary;
final bool showSwap;
final String chainIconPath;
final String? title;
final String? trailingText;
final AssetDetailsModalModes modalMode;
final WalletBase wallet;
@override
Widget build(BuildContext context) {
final iconPath = _getIconPath();
return GestureDetector(
onTap: (){
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return AssetDetailsModal(
showSwap: showSwap,
asset: balance.asset,
title: title ?? balance.asset.fullName ?? balance.asset.name,
chainTitle: _getChainTitle(),
subtitle: trailingText ?? "",
amount: showSecondary ? balance.secondAvailableBalance : balance.availableBalance,
currencyTitle: balance.asset.title,
fiatAmount: showSecondary
? balance.fiatSecondAvailableBalance
: balance.fiatAvailableBalance,
iconPath: balance.asset.iconPath ?? "",
chainIconPath: chainIconPath,
mode: modalMode,
wallet: wallet,
);
});
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 6.0),
child: Container(
width: double.infinity,
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.surfaceContainerHigh,
Theme.of(context).colorScheme.surfaceContainer,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.all(Radius.circular(20)),
border: Border.all(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 45,
height: 45,
child: Stack(
children: [
if((iconPath).isNotEmpty)
Image.asset(iconPath)
else
Container(
width: 45,
height: 45,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(99999)),
child: Center(
child: Text(
balance.asset.name.substring(0, 2),
style: TextStyle(
fontSize: 20, color: Theme.of(context).colorScheme.onPrimary),
)),
),
if (chainIconPath.isNotEmpty)
Align(
alignment: Alignment.bottomRight,
child: Container(
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(5),side: BorderSide(color: Colors.black)),
color: Colors.white),
child: Padding(
padding: const EdgeInsets.all(2.0),
child: SvgPicture.asset(
chainIconPath,
width: 12,
height: 12,
colorFilter:
ColorFilter.mode(Colors.black, BlendMode.srcIn),
),
)))
],
)),
SizedBox(width: 8.0),
Expanded(
child: Column(
spacing: 4.0,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 4,
children: [
Text(
title ?? balance.asset.fullName ?? balance.asset.name,
style: TextStyle(fontWeight: FontWeight.bold),
),
if(trailingText != null)
Text(
trailingText!,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
"${showSecondary ?balance.secondAvailableBalance: balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}",
maxLines:1,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
],
),
),
],
),
),
Text(
showSecondary ? balance.fiatSecondAvailableBalance : balance.fiatAvailableBalance,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
),
),
);
}
String _getChainTitle() {
try {
return CryptoCurrency.fromString(wallet.currency.tag ??wallet.currency.title).title;
} catch(e) {
return wallet.currency.title;
}
}
String _getIconPath() {
if(balance.asset == CryptoCurrency.baseEth)
return "assets/images/crypto/ethereum.webp";
return balance.asset.iconPath ?? "";
}
}
@@ -0,0 +1,88 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'assets_section.dart';
import 'history_section.dart';
class AssetsHistorySectionTab {
final String title;
final Widget content;
AssetsHistorySectionTab(this.title, this.content);
}
class AssetsHistorySection extends StatefulWidget {
AssetsHistorySection({super.key, required this.dashboardViewModel, required this.nftViewModel});
final DashboardViewModel dashboardViewModel;
final NFTViewModel nftViewModel;
@override
State<AssetsHistorySection> createState() => _AssetsHistorySectionState();
}
class _AssetsHistorySectionState extends State<AssetsHistorySection> {
List<AssetsHistorySectionTab> tabs = [];
int _selectedTab = 0;
void reloadTabs() {
final oldTabLength = tabs.length;
tabs = [
if (widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled || (widget.dashboardViewModel.hasMweb && widget.dashboardViewModel.mwebEnabled))
AssetsHistorySectionTab(
S.current.assets,
AssetsSection(
dashboardViewModel: widget.dashboardViewModel,
)),
AssetsHistorySectionTab(
S.current.history,
HistorySection(
dashboardViewModel: widget.dashboardViewModel,
)),
if (isNFTACtivatedChain(widget.dashboardViewModel.wallet.type,
chainId: widget.dashboardViewModel.wallet.chainId))
AssetsHistorySectionTab(S.current.nfts, NFTListingPage(nftViewModel: widget.nftViewModel))
];
if (oldTabLength != tabs.length) {
setState(() {
_selectedTab = 0;
});
}
}
@override
void initState() {
super.initState();
reloadTabs();
reaction((_)=>widget.dashboardViewModel.balanceViewModel.formattedBalances, (value) {
reloadTabs();
});
reaction((_)=>widget.dashboardViewModel.mwebEnabled, (_)=>reloadTabs());
}
@override
Widget build(BuildContext context) {
return Column(
children: [
if(tabs.length>1)
AssetsTopBar(
dashboardViewModel: widget.dashboardViewModel,
tabs: tabs.map((item) => item.title).toList(),
onTabChange: (index) {
setState(() {
_selectedTab = index;
});
},
selectedTab: _selectedTab,
),
tabs[_selectedTab].content
],
);
}
}
@@ -0,0 +1,69 @@
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'asset_tile.dart';
class AssetsSection extends StatelessWidget {
const AssetsSection({super.key, required this.dashboardViewModel});
final DashboardViewModel dashboardViewModel;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 64.0),
child: Observer(
builder: (context) {
final hasMweb = dashboardViewModel.hasMweb && dashboardViewModel.mwebEnabled;
return ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: NeverScrollableScrollPhysics(),
itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length + (hasMweb ? 1 : 0),
itemBuilder: (context, index) {
return Observer(builder: (context) {
if (hasMweb) {
return AssetTile(
showSwap: dashboardViewModel.isEnabledSwapAction,
balance: dashboardViewModel.balanceViewModel.formattedBalances.first,
showSecondary: index > 0 ? true : false,
wallet: dashboardViewModel.wallet,
chainIconPath: index > 0 ? dashboardViewModel.wallet.currency.chainIconPath! : "",
trailingText: index > 0 ? "MWEB" : null,
modalMode: index > 0
? AssetDetailsModalModes.ltcPrivate
: AssetDetailsModalModes.ltcTransparent,
title: index > 0 ? "Litecoin Private" : null,
);
}
final balance = dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index);
return AssetTile(
showSwap: dashboardViewModel.isEnabledSwapAction,
balance: balance,
wallet: dashboardViewModel.wallet,
chainIconPath: _getChainIconPath(),
);
});
},
);
}
),
);
}
// TODO refactor chainIconPath to get rid of this ugly thing. it's needed because arbitrum's wallet currency isn't arb
String _getChainIconPath() {
try {
return CryptoCurrency.fromString(dashboardViewModel.wallet.currency.tag ??dashboardViewModel.wallet.currency.title).chainIconPath!;
} catch(e) {
return dashboardViewModel.wallet.currency.chainIconPath ?? "";
}
}
}
@@ -0,0 +1,76 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/line_tab_switcher.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class AssetsTopBar extends StatelessWidget {
const AssetsTopBar({
super.key,
required this.onTabChange,
required this.selectedTab,
required this.tabs, required this.dashboardViewModel,
});
final void Function(int) onTabChange;
final int selectedTab;
final List<String> tabs;
final DashboardViewModel dashboardViewModel;
@override
Widget build(BuildContext context) {
final hasTokenSettingsButton = tabs[selectedTab] == S.of(context).assets && dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled;
return Padding(
padding: const EdgeInsets.only(top: 12.0, bottom: 12.0, left: 12.0, right: 18.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LineTabSwitcher(
tabs: tabs,
onTabChange: onTabChange,
selectedTab: selectedTab,
),
Opacity(
opacity: hasTokenSettingsButton ? 1 : 0,
child: ElevatedButton(
onPressed: () {
if (hasTokenSettingsButton) {
Navigator.of(context).pushNamed(
Routes.homeSettings,
arguments: dashboardViewModel.balanceViewModel,
);
}
},
style: ElevatedButton.styleFrom(
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(999999),
),
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainer,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
spacing: 4.0,
children: [
SvgPicture.asset("assets/new-ui/options_slider.svg", colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary,BlendMode.srcIn)),
Text(
S.of(context).tokens,
style: TextStyle(color: Theme.of(context).colorScheme.primary),
)
],
),
),
),
),
],
),
);
}
}
@@ -0,0 +1,43 @@
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
class HistoryOrderTile extends StatelessWidget {
const HistoryOrderTile(
{super.key,
required this.date,
required this.amount,
required this.amountFiat,
required this.roundedTop,
required this.roundedBottom,
required this.bottomSeparator,
});
final String date;
final String amount;
final String amountFiat;
final bool roundedTop;
final bool roundedBottom;
final bool bottomSeparator;
@override
Widget build(BuildContext context) {
return HistoryTileBase(
title: "Order",
date: date,
amount: amount,
amountFiat: amountFiat,
leadingIcon:Image.asset("assets/images/cakepay.png"),
roundedTop: roundedTop,
roundedBottom: roundedBottom,
bottomSeparator: bottomSeparator,
);
}
}
@@ -0,0 +1,168 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/anonpay_history_tile.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_order_tile.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_trade_tile.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/payjoin_history_tile.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/utils/date_formatter.dart';
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/date_section_item.dart';
import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:intl/intl.dart';
class HistorySection extends StatelessWidget {
const HistorySection({super.key, required this.dashboardViewModel});
final DashboardViewModel dashboardViewModel;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Observer(
builder: (_) => (dashboardViewModel.items.isEmpty &&
dashboardViewModel.status is! SyncingSyncStatus)
? Padding(
padding: EdgeInsets.only(top: 24),
child: Text(S.of(context).transactions_will_appear_here,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant)))
: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: dashboardViewModel.items.length,
itemBuilder: (context, index) => Observer(builder: (_) {
final prevItem = index == 0 ? null : dashboardViewModel.items[index - 1];
final item = dashboardViewModel.items[index];
final nextItem = index == dashboardViewModel.items.length - 1
? null
: dashboardViewModel.items[index + 1];
final roundedBottom = (nextItem == null || nextItem is DateSectionItem);
final roundedTop = (prevItem == null || prevItem is DateSectionItem);
if (item is TransactionListItem) {
final transaction = item.transaction;
final transactionType = dashboardViewModel.getTransactionType(transaction);
if (item.hasTokens && item.assetOfTransaction == null) {
return Container();
}
CryptoCurrency? asset;
if (transaction.additionalInfo["isLightning"] == true)
asset = CryptoCurrency.btcln;
return GestureDetector(
onTap: () => Navigator.of(context)
.pushNamed(Routes.transactionDetails, arguments: transaction),
child: HistoryTile(
title: item.formattedTitle + transactionType,
date: DateFormat('HH:mm').format(transaction.date),
amount: item.formattedCryptoAmount,
amountFiat: item.formattedFiatAmount,
roundedBottom: roundedBottom,
roundedTop: roundedTop,
bottomSeparator: !roundedBottom,
direction: item.transaction.direction,
pending: item.transaction.isPending,
asset: asset,
),
);
} else if (item is TradeListItem) {
final trade = item.trade;
final tradeFrom = trade.fromRaw >= 0 ? trade.from : trade.userCurrencyFrom;
final tradeTo = trade.toRaw >= 0 ? trade.to : trade.userCurrencyTo;
return GestureDetector(
onTap: () => Navigator.of(context)
.pushNamed(Routes.tradeDetails, arguments: trade),
child: HistoryTradeTile(
from: tradeFrom!,
to: tradeTo!,
date: DateFormat('HH:mm').format(item.trade.createdAt!),
amount: trade.amountFormatted(),
receiveAmount: trade.receiveAmountFormatted(),
roundedBottom: roundedBottom,
roundedTop: roundedTop,
bottomSeparator: !roundedBottom,
swapState: trade.state,
),
);
} else if (item is DateSectionItem) {
return Padding(
padding: EdgeInsets.only(left: 8.0, bottom: 8.0, top: 18.0),
child: Text(DateFormatter.convertDateTimeToReadableString(item.date),
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)));
}else if(item is OrderListItem){
return GestureDetector(
onTap: () => Navigator.of(context)
.pushNamed(Routes.orderDetails, arguments: item.order),
child: HistoryOrderTile(
date: DateFormat('HH:mm').format(item.order.createdAt),
amount: item.orderFormattedAmount,
amountFiat: "USD 0.00",
roundedBottom: roundedBottom,
roundedTop: roundedTop,
bottomSeparator: !roundedBottom,
),
);
} else if (item is PayjoinTransactionListItem) {
final session = item.session;
return GestureDetector(
onTap: () => Navigator.of(context).pushNamed(
Routes.payjoinDetails,
arguments: [item.sessionId, item.transaction],
),
child: PayjoinHistoryTile(
createdAt: DateFormat('HH:mm').format(session.inProgressSince!),
amount: dashboardViewModel.appStore.amountParsingProxy
.getDisplayCryptoString(session.amount.toInt(), CryptoCurrency.btc),
currency: item.transaction?.from ?? "BTC",
state: item.status,
isSending: session.isSenderSession,
roundedTop: roundedTop,
roundedBottom: roundedBottom,
bottomSeparator: !roundedBottom),
);
} else if (item is AnonpayTransactionListItem) {
final transactionInfo = item.transaction;
return GestureDetector(
onTap: () => Navigator.of(context)
.pushNamed(Routes.anonPayDetailsPage, arguments: transactionInfo),
child: AnonpayHistoryTile(
provider: transactionInfo.provider,
createdAt: DateFormat('HH:mm').format(transactionInfo.createdAt),
amount: transactionInfo.fiatAmount?.toString() ??
(transactionInfo.amountTo?.toString() ?? ''),
currency: transactionInfo.fiatAmount != null
? transactionInfo.fiatEquiv ?? ''
: CryptoCurrency.fromFullName(transactionInfo.coinTo).name.toUpperCase(),
roundedTop: roundedTop,
roundedBottom: roundedBottom,
bottomSeparator: !roundedBottom));
} else
return Text(item.runtimeType.toString());
}),
),
),
);
}
}
@@ -0,0 +1,118 @@
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class HistoryTile extends StatelessWidget {
const HistoryTile({
super.key,
required this.title,
required this.date,
required this.amount,
required this.amountFiat,
required this.roundedTop,
required this.roundedBottom,
required this.direction,
required this.pending,
required this.bottomSeparator,
this.asset,
});
final String title;
final String date;
final String amount;
final String amountFiat;
final bool roundedTop;
final bool roundedBottom;
final bool bottomSeparator;
final TransactionDirection direction;
final bool pending;
final CryptoCurrency? asset;
String _getDirectionIcon() {
if (pending) {
return direction == TransactionDirection.incoming
? 'assets/new-ui/history-receiving.svg'
: 'assets/new-ui/history-sending.svg';
} else {
return direction == TransactionDirection.incoming
? 'assets/new-ui/history-received.svg'
: 'assets/new-ui/history-sent.svg';
}
}
String _getDirectionIconToken() {
if (pending) {
return direction == TransactionDirection.incoming
? 'assets/new-ui/history-receiving.svg'
: 'assets/new-ui/history-sending.svg';
} else {
return direction == TransactionDirection.incoming
? 'assets/new-ui/token-received.svg'
: 'assets/new-ui/token-sent.svg';
}
}
Color? _getPrimaryTextColor() {
}
Widget _getLeadingIcon(BuildContext context) {
if (asset == CryptoCurrency.btcln) {
return Stack(
children: [
SvgPicture.asset(
"assets/images/lightning-icon.svg",
width: 34,
height: 34,
),
Positioned(
top: 20,
left: 20,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(color: Theme.of(context).colorScheme.onInverseSurface, shape: BoxShape.circle),
child: SvgPicture.asset(
_getDirectionIconToken(),
colorFilter: ColorFilter.mode(
direction == TransactionDirection.outgoing
? Theme
.of(context)
.colorScheme
.inverseSurface.withAlpha(175)
: Colors.green,
BlendMode.srcIn),
width: 14,
height: 14,
),
),
)
],
);
}
return SvgPicture.asset(_getDirectionIcon(),
colorFilter: ColorFilter.mode(
direction == TransactionDirection.outgoing
? Theme.of(context).colorScheme.inverseSurface.withAlpha(175)
: Colors.green,
BlendMode.srcIn));
}
@override
Widget build(BuildContext context) {
return HistoryTileBase(
title: title,
date: date,
amount: amount,
amountFiat: amountFiat,
leadingIcon: _getLeadingIcon(context),
primaryTextColor: _getPrimaryTextColor(),
roundedTop: roundedTop,
roundedBottom: roundedBottom,
bottomSeparator: bottomSeparator,
asset: asset,
);
}
}
@@ -0,0 +1,143 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
class HistoryTileBase extends StatelessWidget {
const HistoryTileBase({
super.key,
required this.title,
required this.date,
required this.amount,
required this.leadingIcon,
required this.amountFiat,
required this.roundedTop,
required this.roundedBottom,
required this.bottomSeparator,
this.primaryTextColor,
this.asset,
});
final String title;
final String date;
final String amount;
final String amountFiat;
final Widget leadingIcon;
final bool roundedTop;
final bool roundedBottom;
final bool bottomSeparator;
final Color? primaryTextColor;
final CryptoCurrency? asset;
// String _getDirectionIcon() {
// if (pending) {
// return direction == TransactionDirection.incoming
// ? 'assets/new-ui/history-receiving.svg'
// : 'assets/new-ui/history-sending.svg';
// } else {
// return direction == TransactionDirection.incoming
// ? 'assets/new-ui/history-received.svg'
// : 'assets/new-ui/history-sent.svg';
// }
// }
//
// Widget _getLeadingIcon(BuildContext context) {
// if (asset == CryptoCurrency.btcln) {
// return Stack(
// children: [
// Image.asset(
// asset!.iconPath!,
// width: 34,
// height: 34,
// ),
// Positioned(
// top: 20,
// left: 20,
// child: SvgPicture.asset(
// 'assets/new-ui/chain_badges/lightning.svg',
// width: 16,
// height: 16,
// ),
// )
// ],
// );
// }
//
// return SvgPicture.asset(_getDirectionIcon());
// }
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(roundedTop ? 22 : 0),
topRight: Radius.circular(roundedTop ? 22 : 0),
bottomLeft: Radius.circular(roundedBottom ? 22 : 0),
bottomRight: Radius.circular(roundedBottom ? 22 : 0),
)),
color: Theme.of(context).colorScheme.onInverseSurface,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 12,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(right: 10),
child: SizedBox(
height: 36,
width: 36,
child: leadingIcon,
),
),
Expanded(
child: Padding(
padding: EdgeInsets.only(right: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
Text(date,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant)),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(amount,
style: TextStyle(
color: primaryTextColor ??
Theme.of(context).colorScheme.onSurface)),
Text(amountFiat, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
],
),
],
),
),
),
],
),
),
),
if(bottomSeparator) Container(
color: Theme.of(context).colorScheme.onInverseSurface,
child: Padding(
padding: EdgeInsets.only(left: 56, right: 16),
child: Container(height: 1, color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175)),
),
)
],
);
}
}
@@ -0,0 +1,94 @@
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
class HistoryTradeTile extends StatelessWidget {
const HistoryTradeTile(
{super.key,
required this.date,
required this.amount,
required this.receiveAmount,
required this.roundedTop,
required this.roundedBottom,
required this.bottomSeparator,
required this.from,
required this.to,
required this.swapState});
final CryptoCurrency from;
final CryptoCurrency to;
final String date;
final String amount;
final String receiveAmount;
final bool roundedTop;
final bool roundedBottom;
final bool bottomSeparator;
final TradeState swapState;
Widget _getLeadingStack(BuildContext context) {
double currencyIconSize = 22.0;
return SizedBox(
height: 50,
width: 50,
child: Stack(
children: [
Image.asset(_getIconPath(from),
width: currencyIconSize, height: currencyIconSize),
Positioned(
top: currencyIconSize / 2,
left: currencyIconSize / 2,
child: Container(
decoration: BoxDecoration(
border: Border.all(
width: 2,
color: Theme.of(context).colorScheme.surfaceContainer),
shape: BoxShape.circle),
child: Image.asset(_getIconPath(to),
width: currencyIconSize, height: currencyIconSize))),
],
),
);
}
@override
Widget build(BuildContext context) {
return HistoryTileBase(
title:
"${from.toString()}${from == CryptoCurrency.btcln ? "-LN" : ""} → ${to.toString()}${to == CryptoCurrency.btcln ? "-LN" : ""}",
date: date,
amount: amount,
amountFiat: receiveAmount,
leadingIcon: _getLeadingStack(context),
roundedTop: roundedTop,
roundedBottom: roundedBottom,
bottomSeparator: bottomSeparator,
);
}
String _getIconPath(CryptoCurrency currency) {
if (currency.iconPath != null) {
return currency.iconPath!;
}
if (currency.name.isNotEmpty) {
final currencyFromName = CryptoCurrency.fromString(currency.name);
if (currencyFromName.iconPath != null) {
return currencyFromName.iconPath!;
}
}
if (currency.title.isNotEmpty) {
final currencyFromTitle = CryptoCurrency.fromString(currency.title);
if (currencyFromTitle.iconPath != null) {
return currencyFromTitle.iconPath!;
}
}
//TODO approporiate fallback
return "";
}
}
@@ -0,0 +1,45 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile_base.dart';
import 'package:flutter/material.dart';
class PayjoinHistoryTile extends StatelessWidget {
const PayjoinHistoryTile(
{super.key,
required this.createdAt,
required this.amount,
required this.currency,
required this.state,
required this.isSending,
required this.roundedTop,
required this.roundedBottom,
required this.bottomSeparator});
final String createdAt;
final String amount;
final String currency;
final String state;
final bool isSending;
final bool roundedTop;
final bool roundedBottom;
final bool bottomSeparator;
@override
Widget build(BuildContext context) {
return HistoryTileBase(
title: "${isSending ? S.of(context).outgoing : S.of(context).incoming} Payjoin",
date: createdAt,
amount: amount + " " + currency,
leadingIcon: ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset(
'assets/images/payjoin.png',
width: 36,
height: 36,
),
),
amountFiat: state,
roundedTop: roundedTop,
roundedBottom: roundedBottom,
bottomSeparator: bottomSeparator);
}
}
@@ -0,0 +1,307 @@
import 'package:cw_core/card_design.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class BalanceCardAction {
final String label;
final IconData icon;
final VoidCallback onTap;
const BalanceCardAction({required this.label, required this.icon, required this.onTap});
}
class BalanceCard extends StatelessWidget {
const BalanceCard({
super.key,
required this.width,
required this.design,
this.gradient,
this.borderRadius = 20,
this.selected = false,
this.accountName = "",
this.accountBalance = "",
this.balance = "",
this.fiatBalance = "",
this.assetName = "",
this.designSwitchDuration = const Duration(),
this.actions = const [],
this.capitalizeAssetName = true,
this.onCustomizeTapped,
this.accountIndex
});
final double width;
final double borderRadius;
final Gradient? gradient;
final String accountBalance;
final String accountName;
final String balance;
final String fiatBalance;
final int? accountIndex;
final bool capitalizeAssetName;
final String assetName;
final bool selected;
final CardDesign design;
final List<BalanceCardAction> actions;
final Duration designSwitchDuration;
final VoidCallback? onCustomizeTapped;
@override
Widget build(BuildContext context) {
final Duration textFadeDuration = Duration(milliseconds: 80);
final double iconWidth = width * 0.15;
final bool showText = accountBalance.isNotEmpty ||
accountName.isNotEmpty ||
balance.isNotEmpty ||
fiatBalance.isNotEmpty ||
assetName.isNotEmpty;
final height = width * 0.62;
return AnimatedContainer(
duration: designSwitchDuration,
width: width,
height: height,
decoration: ShapeDecoration(
gradient: gradient ?? design.gradient,
shape: RoundedSuperellipseBorder(
side: const BorderSide(color: Color(0x44FFFFFF), width: 1),
borderRadius: BorderRadiusGeometry.circular(borderRadius),
),
),
child: Stack(
children: [
AnimatedSwitcher(
duration: designSwitchDuration,
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
child: design.backgroundType == CardDesignBackgroundTypes.svgFull
? ClipRSuperellipse(
borderRadius: BorderRadius.circular(borderRadius),
key: ValueKey(design.imagePath),
child: SvgPicture.asset(
design.imagePath,
width: width,
height: height,
fit: BoxFit.fill,
),
)
: const SizedBox.shrink(
key: ValueKey('svgFullOff'),
),
),
Padding(
padding: EdgeInsets.all(width * 0.05),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
if (showText)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
if(accountName.isNotEmpty || accountBalance.isNotEmpty)
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
spacing: 4,
children: [
if (accountIndex != null)
Opacity(
opacity: 0.5,
child: AnimatedDefaultTextStyle(
duration: designSwitchDuration,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w500,
color: design.colors.textColor),
child: Text("$accountIndex."),
),
),
AnimatedDefaultTextStyle(
duration: designSwitchDuration,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w500, color: design.colors.textColor),
child: Text(accountName),
),
],
),
AnimatedOpacity(
opacity: selected ? 0 : 1,
duration: textFadeDuration,
child: Text(
accountBalance,
style: TextStyle(color: design.colors.textColor, fontSize: 14),
),
),
],
),
AnimatedOpacity(
opacity: selected ? 1 : 0,
duration: textFadeDuration,
child: AnimatedSwitcher(
duration: designSwitchDuration,
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
child: Row(
key: ValueKey("$balance ${assetName.toUpperCase()}"),
spacing: 8.0,
children: [
AnimatedDefaultTextStyle(
duration: designSwitchDuration,
style: DefaultTextStyle.of(context)
.style
.copyWith(color: design.colors.textColor, fontSize: 28, fontWeight: FontWeight.w500, letterSpacing: -0.4),
child: Text(balance),
),
AnimatedDefaultTextStyle(
duration: designSwitchDuration,
style: DefaultTextStyle.of(context)
.style
.copyWith(color: design.colors.textColorSecondary, fontSize: 28, fontWeight: FontWeight.w400, letterSpacing: -0.4),
child: Text(capitalizeAssetName ? assetName.toUpperCase() : assetName.toLowerCase()),
),
],
),
),
),
AnimatedDefaultTextStyle(
duration: designSwitchDuration,
style: DefaultTextStyle.of(context).style.copyWith(
fontSize: 15,
fontWeight: FontWeight.w400,
color: design.colors.textColorSecondary),
child: AnimatedSwitcher(
duration: designSwitchDuration,
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
child: Text(
key: ValueKey(fiatBalance),
fiatBalance,
),
),
),
],
)
else
Container(),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
AnimatedSwitcher(
duration: designSwitchDuration,
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
child: Row(
key: ValueKey(actions.toString()),
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.start,
children: actions.map(getBalanceCardActionButton).toList(),
),
),
AnimatedSwitcher(
duration: designSwitchDuration,
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
child: design.backgroundType == CardDesignBackgroundTypes.svgIcon
? SvgPicture.asset(
design.imagePath,
key: const ValueKey('svgIcon'),
height: iconWidth,
width: iconWidth,
colorFilter: ColorFilter.mode(
design.colors.backgroundImageColor.withAlpha(80),
BlendMode.srcIn,
),
)
: const SizedBox.shrink(
key: ValueKey('svgIconOff'),
),
),
],
),
],
),
),
Positioned(
right: 8,
top: 8,
child: AnimatedOpacity(
duration: designSwitchDuration,
opacity: onCustomizeTapped == null ? 0 : 1,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onCustomizeTapped,
child: SizedBox(
height: 40,
width: 40,
child: Center(
child: SvgPicture.asset(
"assets/new-ui/3dots_vertical.svg",
colorFilter:
ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn),
),
),
),
),
),
)
],
),
);
}
Widget getBalanceCardActionButton(BalanceCardAction action) => GestureDetector(
onTap: action.onTap,
child: Container(
decoration: BoxDecoration(
color: design.colors.backgroundImageColor.withAlpha(75),
borderRadius: BorderRadius.circular(10000000),
),
margin: const EdgeInsets.only(right: 10),
padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(right: 6),
child: Text(
action.label,
style: TextStyle(color: design.colors.textColor, fontSize: 16),
),
),
Icon(action.icon, color: design.colors.textColorSecondary),
],
),
),
);
}
@@ -0,0 +1,386 @@
import 'dart:math';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/balance_display_mode.dart';
import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/modal_navigator.dart';
import 'package:cake_wallet/new-ui/pages/send_page.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'balance_card.dart';
class CardsView extends StatefulWidget {
const CardsView(
{super.key,
required this.dashboardViewModel,
required this.accountListViewModel,
required this.lightningMode,
required this.onCompactModeBackgroundCardsTapped, required this.onCustomizeTapped});
final DashboardViewModel dashboardViewModel;
final MoneroAccountListViewModel? accountListViewModel;
final VoidCallback onCompactModeBackgroundCardsTapped;
final VoidCallback onCustomizeTapped;
final bool lightningMode;
@override
_CardsViewState createState() => _CardsViewState();
}
class _CardsViewState extends State<CardsView> {
late int _selectedIndex;
bool isFirstBuild = true;
@override
void initState() {
super.initState();
_selectedIndex = widget.dashboardViewModel.cardOrder.length - 1;
reaction(
(_) => widget.dashboardViewModel.cardOrder.values.toList(),
(_) => setState(() {
_selectedIndex = widget.dashboardViewModel.cardOrder.length - 1;
}));
}
static const Duration animDuration = Duration(milliseconds: 200);
static const int compactModeTreshold = 4;
static const int maxCards = 5;
late final double cardWidth = MediaQuery.of(context).size.width * 0.878;
late final double effectiveCardWidth = min(cardWidth, 768);
Widget _buildCard(int visualIndex, int realIndex, int numCards, double parentWidth,
Map<int, int> order, bool compactMode, double overlapAmount) {
final baseTop = overlapAmount * (numCards - 1);
final scaleFactor = compactMode ? 1 : 0.96;
final howFarBehind = (_selectedIndex - visualIndex + numCards) % numCards;
final scale = pow(scaleFactor, howFarBehind).toDouble();
final top = baseTop - (howFarBehind * overlapAmount);
final left = (parentWidth - effectiveCardWidth) / 2.0;
return AnimatedPositioned(
key: ValueKey("$visualIndex $realIndex"),
duration: animDuration,
curve: Curves.easeOut,
top: top,
left: left,
child: AnimatedScale(
duration: animDuration,
curve: Curves.easeOut,
scale: scale,
child: GestureDetector(
onTap: () {
// printV(visualIndex);
if (compactMode && visualIndex != 0) {
widget.onCompactModeBackgroundCardsTapped();
} else if(!compactMode) {
setState(() {
if (widget.accountListViewModel != null)
widget.accountListViewModel!
.select(widget.accountListViewModel!.accounts[realIndex]);
_selectedIndex = visualIndex;
});
}
},
onLongPress: () {
if (_selectedIndex == visualIndex) {
widget.dashboardViewModel.balanceViewModel.switchBalanceValue();
};
HapticFeedback.heavyImpact();
},
child: Observer(builder: (_) {
if(realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) {
return Container();
}
final account = widget.accountListViewModel?.accounts[realIndex];
// The second balance should always be the lightning balance
// printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance);
final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel.formattedBalances
.elementAtOrNull(widget.lightningMode ? 1 : 0);
late final String walletBalance;
late final String walletFiatBalance;
if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) {
if(widget.dashboardViewModel.balanceViewModel.displayMode == BalanceDisplayMode.hiddenBalance) {
walletBalance = '●●●●●●';
walletFiatBalance = '●●●●●●';
} else {
walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0";
walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
}
} else {
walletBalance = walletBalanceRecord?.availableBalance ?? "0";
walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "${widget.dashboardViewModel.appStore.settingsStore.fiatCurrency.title} 0.00";
}
// the card designs is empty if widget gets built before it loads.
// should get populated before user sees anything
final CardDesign cardDesign;
if (widget.dashboardViewModel.cardDesigns.isEmpty ||
realIndex >= widget.dashboardViewModel.cardDesigns.length)
cardDesign = CardDesign.genericDefault;
else if(widget.lightningMode)
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1];
else
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
final String accountName;
final String accountBalance;
if (account == null) {
accountName = "";
accountBalance = "";
} else {
accountName = account.label;
accountBalance = account.balance ?? "0.00";
}
final List<BalanceCardAction> actions = widget.lightningMode
? [
BalanceCardAction(
label: S.current.bitcoin_lightning_deposit,
icon: Icons.arrow_downward,
onTap: depositToL2,
),
BalanceCardAction(
label: S.current.bitcoin_lightning_withdraw,
icon: Icons.arrow_upward,
onTap: withdrawFromL2,
)
]
: widget.dashboardViewModel.isEnabledTradeAction
? [
BalanceCardAction(
label: S.current.buy,
icon: Icons.arrow_forward,
onTap: () => Navigator.of(context).pushNamed(Routes.buySellPage),
)
]
: [];
return BalanceCard(
width: effectiveCardWidth,
accountName: accountName,
accountBalance: accountBalance,
designSwitchDuration: Duration(milliseconds: 150),
assetName: walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback,
capitalizeAssetName: _shouldCapitalizeAssetName(),
balance: walletBalance,
fiatBalance: walletFiatBalance,
selected: _selectedIndex == visualIndex,
onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null,
design: cardDesign,
actions: actions,
);
}),
),
),
);
}
String get assetTitleFallback => widget.dashboardViewModel.appStore.amountParsingProxy.getCryptoSymbol(
widget.lightningMode
? CryptoCurrency.btcln
: widget.dashboardViewModel.wallet.currency);
bool _shouldCapitalizeAssetName() {
if(widget.dashboardViewModel.wallet.type != WalletType.bitcoin) {
return true;
}
switch (widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi) {
case BitcoinAmountDisplayMode.satoshi:
return false;
case BitcoinAmountDisplayMode.satoshiForLightning:
return !widget.lightningMode;
case BitcoinAmountDisplayMode.bitcoin:
return true;
default:
return true;
}
}
double _getBoxHeight(int numCards, double overlapAmount) {
return
/* height of initial card */
(2 / 3.2) * (effectiveCardWidth) +
/* height of bg card * amount of bg cards */
overlapAmount * ((numCards) - 1);
}
@override
Widget build(BuildContext context) {
return Observer(builder: (context) {
final parentWidth = MediaQuery.of(context).size.width;
final children = <Widget>[];
int numCards = widget.dashboardViewModel.wallet.type == WalletType.bitcoin
? 1
: widget.dashboardViewModel.cardDesigns.length;
if(numCards == 0) numCards = 1;
if (_selectedIndex >= (numCards)) {
_selectedIndex = 0;
}
Map<int, int> order = widget.dashboardViewModel.cardOrder.length != numCards
? Map<int, int>.fromEntries(
List.generate(numCards, (i) => MapEntry(i, i)),
)
: widget.dashboardViewModel.cardOrder;
for (int i = min(numCards - 1, maxCards); i >= 0; i--) {
int visualIndex = (_selectedIndex - i + numCards) % numCards;
if (order[visualIndex] == null) {
order = Map<int, int>.fromEntries(
List.generate(numCards, (i) => MapEntry(i, i)),
);
}
}
final bool compactMode = numCards >= compactModeTreshold;
final double overlapAmount = compactMode ? 5.0 : 46.0;
for (int i = min(numCards - 1, maxCards); i >= 0; i--) {
int visualIndex = (_selectedIndex - i + numCards) % numCards;
int realIndex = order[visualIndex]!;
if (visualIndex == _selectedIndex &&
widget.accountListViewModel != null &&
widget.accountListViewModel?.selected.label !=
widget.accountListViewModel?.accounts[realIndex].label) {
widget.accountListViewModel!
.select(widget.accountListViewModel!.accounts[realIndex]);
}
children.add(_buildCard(
visualIndex, realIndex, numCards, parentWidth, order, compactMode, overlapAmount));
}
return AnimatedContainer(
duration: Duration(milliseconds: 200),
curve: Curves.easeOut,
width: double.infinity,
height: _getBoxHeight(numCards, overlapAmount),
child: AnimatedSwitcher(
duration: Duration(milliseconds: 200),
transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child),
child: SizedBox(
key: ValueKey(_getBoxHeight(numCards, overlapAmount)),
width: double.infinity,
height: _getBoxHeight(numCards, overlapAmount),
child: Stack(alignment: Alignment.center, children: children),
),
),
);
});
}
Future<void> depositToL2() async {
PaymentRequest? paymentRequest = null;
if (widget.dashboardViewModel.type == WalletType.litecoin) {
final depositAddress = bitcoin!.getUnusedMwebAddress(widget.dashboardViewModel.wallet);
if ((depositAddress?.isNotEmpty ?? false)) {
paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$depositAddress"));
}
} else if (widget.dashboardViewModel.type == WalletType.bitcoin) {
final depositAddress =
await bitcoin!.getUnusedSpakDepositAddress(widget.dashboardViewModel.wallet);
if ((depositAddress?.isNotEmpty ?? false)) {
paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$depositAddress"));
}
}
if (FeatureFlag.hasNewUiExtraPages && widget.dashboardViewModel.type == WalletType.bitcoin) {
final page = getIt.get<NewSendPage>(
param1: SendPageParams(
initialPaymentRequest: paymentRequest,
unspentCoinType: UnspentCoinType.nonMweb,
mode: SendPageModes.lightningDeposit,
));
showCupertinoModalBottomSheet(context: context, barrierColor: Colors.black.withAlpha(128), builder: (context){
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: SizedBox(
height:MediaQuery.of(context).size.height*0.6,
child:ModalNavigator(parentContext:context,rootPage: Material(child: page))
),
);
});
} else {
Navigator.pushNamed(
context,
Routes.send,
arguments: {
'paymentRequest': paymentRequest,
'coinTypeToSpendFrom': UnspentCoinType.nonMweb,
},
);
}
}
Future<void> withdrawFromL2() async {
PaymentRequest? paymentRequest = null;
UnspentCoinType unspentCoinType = UnspentCoinType.any;
final withdrawAddress = bitcoin!.getUnusedSegwitAddress(widget.dashboardViewModel.wallet);
if (widget.dashboardViewModel.type == WalletType.litecoin) {
if ((withdrawAddress?.isNotEmpty ?? false)) {
paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$withdrawAddress"));
}
unspentCoinType = UnspentCoinType.mweb;
} else if (widget.dashboardViewModel.type == WalletType.bitcoin) {
if ((withdrawAddress?.isNotEmpty ?? false)) {
paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$withdrawAddress"));
}
unspentCoinType = UnspentCoinType.lightning;
}
if (FeatureFlag.hasNewUiExtraPages && widget.dashboardViewModel.type == WalletType.bitcoin) {
final page = getIt.get<NewSendPage>(
param1: SendPageParams(
initialPaymentRequest: paymentRequest,
unspentCoinType: unspentCoinType,
mode: SendPageModes.lightningWithdrawal,
));
showCupertinoModalBottomSheet(context: context, barrierColor: Colors.black.withAlpha(128), builder: (context){
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: SizedBox(
height:MediaQuery.of(context).size.height*0.6,
child:ModalNavigator(parentContext:context,rootPage: Material(child: page))
),
);
});
} else {
Navigator.pushNamed(
context,
Routes.send,
arguments: {
'paymentRequest': paymentRequest,
'coinTypeToSpendFrom': unspentCoinType,
},
);
}
}
}
@@ -0,0 +1,75 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_svg/flutter_svg.dart';
class MwebAd extends StatelessWidget {
const MwebAd({super.key, required this.dashboardViewModel});
final DashboardViewModel dashboardViewModel;
@override
Widget build(BuildContext context) {
return Observer(builder: (_) {
if (!dashboardViewModel.shouldShowMwebAd) return SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 12, top: 24),
child: Column(
spacing: 12,
children: [
GestureDetector(
onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings),
child: Container(
height: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
color: Theme.of(context).colorScheme.surfaceContainer),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SvgPicture.asset(
"assets/new-ui/settings_row_icons/mweb.svg",
width: 24,
height: 24,
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Text(
S.of(context).mweb_ad,
softWrap: true,
style: TextStyle(fontSize: 12),
),
),
),
Icon(
size: 16,
Icons.arrow_forward_ios,
color: Theme.of(context).colorScheme.primary,
)
],
),
),
),
),
GestureDetector(
onTap: () => dashboardViewModel.dismissMwebAd(false),
child: Text(
S.of(context).do_not_show_anymore,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary),
),
)
],
),
);
});
}
}
@@ -0,0 +1,59 @@
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_svg/svg.dart';
class ChainIcon extends StatelessWidget {
const ChainIcon(
{super.key,
required this.iconPath,
required this.dashboardViewModel,
required this.isSyncHeavy});
final String iconPath;
final bool isSyncHeavy;
final DashboardViewModel dashboardViewModel;
@override
Widget build(BuildContext context) {
return Observer(
builder: (_) {
final progress = dashboardViewModel.status.progress();
final done = !isSyncHeavy || progress >= 1;
return Stack(
children: [
AnimatedOpacity(
duration: Duration(milliseconds: 100),
opacity: done ? 0 : 1,
child: CircularProgressIndicator(
value: progress,
color: Color(0xFFFFB84E),
strokeWidth: 2,
),
),
AnimatedScale(
duration: Duration(milliseconds: 150),
scale: done ? 1 : 0.8,
child: AnimatedSwitcher(
duration: Duration(milliseconds: 150),
child: SvgPicture.asset(
key: ValueKey(progress >= 1),
iconPath,
width: 36,
height: 36,
colorFilter: ColorFilter.mode(
done
? Theme.of(context).colorScheme.primary.withOpacity(0.2)
: Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
),
],
);
},
);
}
}
@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
class LightningSwitcher extends StatelessWidget {
const LightningSwitcher(
{super.key, required this.lightningMode, required this.onLightningSwitchPress});
final bool lightningMode;
final VoidCallback onLightningSwitchPress;
@override
Widget build(BuildContext context) {
return SizedBox(
child: InkWell(
onTap: () {
HapticFeedback.mediumImpact();
onLightningSwitchPress();
},
child: Container(
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)),
color: Theme.of(context).colorScheme.surfaceContainer),
width: 70,
height: 36,
padding: EdgeInsets.symmetric(vertical: 2),
child: Stack(
children: [
AnimatedContainer(
alignment: Alignment.centerRight,
margin: EdgeInsets.only(left: lightningMode ? 36 : 2),
duration: Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
width: 32,
height: 32,
// height: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(9999990.0)),
color: Theme.of(context).colorScheme.primary),
),
Container(
child: Row(
spacing: 2.0,
children: [
SizedBox(),
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: SvgPicture.asset(
key: ValueKey(lightningMode),
'assets/new-ui/switcher-bitcoin.svg',
width: 32,
height: 32,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainer,
BlendMode.srcIn,
),
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: SvgPicture.asset(
key: ValueKey(lightningMode),
'assets/new-ui/switcher-lightning.svg',
width: 32,
height: 32,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
],
),
),
],
),
),
),
);
}
}
@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
class PulsingDot extends StatefulWidget {
const PulsingDot({super.key,});
final double size = 5;
final Color color = const Color(0xFFFFC414);
final Duration fadeOutDuration = const Duration(milliseconds: 2000);
final Duration restDuration = const Duration(milliseconds: 2000);
final double restOpacity = 0.3;
@override
State<PulsingDot> createState() => _PulsingDotState();
}
class _PulsingDotState extends State<PulsingDot>
with SingleTickerProviderStateMixin {
late final AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: widget.fadeOutDuration,
reverseDuration: Duration.zero,
value: 1.0,
);
_loop();
}
Future<void> _loop() async {
while (mounted) {
await controller.animateTo(
widget.restOpacity,
curve: Curves.easeOutQuad,
duration: widget.fadeOutDuration,
);
await Future.delayed(widget.restDuration);
if(!mounted) return;
controller.value = 1.0;
}
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: controller,
child: Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
color: widget.color,
shape: BoxShape.circle,
),
),
);
}
}
@@ -0,0 +1,189 @@
import 'package:cake_wallet/core/sync_status_title.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/sync_status.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_svg/flutter_svg.dart';
class SyncBar extends StatelessWidget {
SyncBar({super.key, required this.dashboardViewModel, required this.isSyncHeavy});
final DashboardViewModel dashboardViewModel;
final bool isSyncHeavy;
static const failStatuses = [
FailedSyncStatus,
LostConnectionSyncStatus,
TimedOutSyncStatus,
UnsupportedSyncStatus,
];
static const progressStatuses = [
SyncingSyncStatus,
NotConnectedSyncStatus,
SyncronizingSyncStatus,
AttemptingSyncStatus,
StartingScanSyncStatus,
AttemptingScanSyncStatus,
SyncedTipSyncStatus,
ProcessingSyncStatus,
ConnectingSyncStatus,
ConnectedSyncStatus,
];
@override
Widget build(BuildContext context) {
return Observer(
builder: (_) {
final status = dashboardViewModel.status;
final Widget? icon = _getIcon(context, status.runtimeType);
return Expanded(
child: Stack(
alignment: Alignment.centerLeft,
children: [
if (!_showFullBar())
Row(
mainAxisSize: MainAxisSize.min,
spacing: 6,
children: [
if (dashboardViewModel.isTorEnabled)
CakeImageWidget(
imageUrl: "assets/new-ui/tor.svg",
width: 20,
height: 20,
),
if (_showDot()) PulsingDot(),
],
),
if (_showFullBar())
GestureDetector(
onTap: () {
Navigator.of(context, rootNavigator: true).pushNamed(Routes.connectionSync);
},
child: AnimatedSwitcher(
duration: Duration(milliseconds: 100),
child: Container(
key: ValueKey(status.runtimeType),
height: 36,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999),
border: _getBorder(context, status.runtimeType),
color: _getBackgroundColor(context, status.runtimeType),
),
child: Row(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
if (icon != null) icon,
// if (dashboardViewModel.silentPaymentsScanningActive &&
// progressStatuses.contains(status.runtimeType)) ...[
// Text(
// "${(status.progress() * 100).toInt()}%",
// style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)),
// ),
// Text(
// "·",
// style: TextStyle(fontSize: 12),
// )
// ],
Text(
syncStatusTitle(
status, dashboardViewModel.settingsStore.syncStatusDisplayMode),
style: _getTextStyle(context, status.runtimeType),
),
],
),
),
),
),
],
),
);
},
);
}
Color? _getBackgroundColor(BuildContext context, Type status) {
if (failStatuses.contains(status)) {
return Theme.of(context).colorScheme.errorContainer.withAlpha(64);
}
return null;
}
Border? _getBorder(BuildContext context, Type status) {
if (progressStatuses.contains(status)) {
return Border.all(color: Theme.of(context).colorScheme.surfaceContainerHigh, width: 1);
}
return null;
}
TextStyle? _getTextStyle(BuildContext context, Type status) {
if (failStatuses.contains(status)) {
return TextStyle(
fontSize: 12, fontWeight: FontWeight.w400, color: Theme.of(context).colorScheme.error);
} else {
return TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant);
}
}
Widget? _getIcon(BuildContext context, Type status) {
if (status == LostConnectionSyncStatus || status == FailedSyncStatus) {
return SvgPicture.asset(
"assets/new-ui/offline.svg",
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn),
);
}
if (failStatuses.contains(status)) {
return SvgPicture.asset(
"assets/new-ui/warning.svg",
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn),
);
}
final List<Widget> children = [];
if (dashboardViewModel.isTorEnabled) {
children.add(SvgPicture.asset("assets/new-ui/tor_sync.svg",
colorFilter: ColorFilter.mode(Color(0xFF8A38F5), BlendMode.srcIn)));
}
if (dashboardViewModel.hasMweb) {
children.add(SvgPicture.asset(
"assets/new-ui/mweb_sync.svg",
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
));
}
if (dashboardViewModel.hasSilentPayments) {
children.add(SvgPicture.asset(
"assets/new-ui/silent_sync.svg",
colorFilter: ColorFilter.mode(Color(0xFFEFBA5E), BlendMode.srcIn),
));
}
return Row(
spacing: 8,
children: children,
);
}
bool _showFullBar() {
if (dashboardViewModel.status.runtimeType == SyncedSyncStatus) return false;
return isSyncHeavy || failStatuses.contains(dashboardViewModel.status.runtimeType);
}
bool _showDot() {
return !isSyncHeavy && progressStatuses.contains(dashboardViewModel.status.runtimeType);
}
}

Some files were not shown because too many files have changed in this diff Show More