From 8dacd12b746e35df05cf613c176efefb9c1a52ee Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Mar 2021 15:31:03 +0300 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle | 16 +- .../java/com/tangem/tap/TapApplication.kt | 3 + .../tap/common/extensions/AssetManager.kt | 6 + .../tap/common/extensions/Blockchain.kt | 23 + .../tap/common/extensions/Navigation.kt | 4 + .../tangem/tap/common/extensions/Specific.kt | 27 - .../tangem/tap/common/extensions/String.kt | 62 +++ .../tangem/tap/common/extensions/TextView.kt | 9 + .../com/tangem/tap/common/extensions/Token.kt | 11 + .../com/tangem/tap/common/extensions/UI.kt | 21 - .../com/tangem/tap/common/redux/AppReducer.kt | 4 +- .../com/tangem/tap/common/redux/AppState.kt | 10 +- .../tap/common/redux/global/GlobalAction.kt | 4 - .../common/redux/global/GlobalMidlleware.kt | 2 +- .../tap/common/redux/global/GlobalReducer.kt | 9 +- .../tap/common/redux/global/GlobalState.kt | 11 - .../redux/navigation/NavigationState.kt | 4 +- .../com/tangem/tap/domain/PayIdManager.kt | 33 -- .../tangem/tap/domain/TapBlockchainConfig.kt | 13 + .../com/tangem/tap/domain/TapWalletManager.kt | 167 ++++--- .../configurable/config/ConfigManager.kt | 25 +- .../domain/configurable/config/JsonModels.kt | 3 + .../tangem/tap/domain/tasks/ScanNoteTask.kt | 29 +- .../tap/domain/tokens/CurrenciesRepository.kt | 148 ++++++ .../features/details/redux/DetailsAction.kt | 2 +- .../details/redux/DetailsMiddleware.kt | 4 +- .../features/details/redux/DetailsReducer.kt | 10 +- .../features/details/redux/DetailsState.kt | 2 +- .../redux/twins/CreateTwinWalletMiddleware.kt | 2 +- .../features/details/ui/DetailsFragment.kt | 1 - .../features/send/redux/SendScreenAction.kt | 2 + .../send/redux/middlewares/SendMiddleware.kt | 10 +- .../send/redux/reducers/SendScreenReducer.kt | 11 +- .../tap/features/send/ui/SendFragment.kt | 4 +- .../stateSubscribers/SendStateSubscriber.kt | 2 +- .../tap/features/tokens/redux/TokensAction.kt | 21 + .../features/tokens/redux/TokensMiddleware.kt | 28 ++ .../features/tokens/redux/TokensReducer.kt | 30 ++ .../tap/features/tokens/redux/TokensState.kt | 18 + .../features/tokens/ui/AddTokensFragment.kt | 119 +++++ .../tokens/ui/adapters/CurrenciesAdapter.kt | 165 ++++++ .../wallet/models/PendingTransaction.kt | 10 + .../tap/features/wallet/redux/WalletAction.kt | 105 ++-- .../features/wallet/redux/WalletMiddleware.kt | 316 ------------ .../features/wallet/redux/WalletReducer.kt | 417 ++++++++-------- .../tap/features/wallet/redux/WalletState.kt | 134 +++-- .../CheckSignedHashesMiddleware.kt | 103 ++++ .../middlewares/MultiWalletMiddleware.kt | 72 +++ .../redux/middlewares/TopUpMiddleware.kt | 29 ++ .../redux/middlewares/TwinsMiddleware.kt | 19 + .../redux/middlewares/WalletMiddleware.kt | 230 +++++++++ .../redux/reducers/MultiWalletReducer.kt | 160 ++++++ .../redux/reducers/OnWalletLoadedReducer.kt | 182 +++++++ .../wallet/redux/reducers/TwinsReducer.kt | 36 ++ .../tap/features/wallet/ui/BalanceWidget.kt | 8 +- .../wallet/ui/MultipleAddressUiHelper.kt | 50 ++ .../wallet/ui/WalletDetailsFragment.kt | 289 +++++++++++ .../tap/features/wallet/ui/WalletFragment.kt | 270 ++-------- .../PendingTransactionsAdapter.kt | 2 +- .../wallet/ui/adapters/WalletAdapter.kt | 137 +++++ .../{ => adapters}/WarningMessagesAdapter.kt | 2 +- .../features/wallet/ui/dialogs/PayIdDialog.kt | 47 -- .../wallet/ui/wallet/MultiWalletView.kt | 126 +++++ .../wallet/ui/wallet/SingleWalletView.kt | 236 +++++++++ .../features/wallet/ui/wallet/WalletView.kt | 18 + app/src/main/res/drawable-hdpi/ic_binance.png | Bin 0 -> 3696 bytes app/src/main/res/drawable-hdpi/ic_btc.png | Bin 0 -> 4495 bytes .../main/res/drawable-hdpi/ic_btc_cash.png | Bin 0 -> 4067 bytes app/src/main/res/drawable-hdpi/ic_cardano.png | Bin 0 -> 4813 bytes app/src/main/res/drawable-hdpi/ic_eth.png | Bin 0 -> 3676 bytes app/src/main/res/drawable-hdpi/ic_ltc.png | Bin 0 -> 3362 bytes app/src/main/res/drawable-hdpi/ic_rsk.png | Bin 0 -> 4508 bytes app/src/main/res/drawable-hdpi/ic_stellar.png | Bin 0 -> 3953 bytes app/src/main/res/drawable-hdpi/ic_tezos.png | Bin 0 -> 3791 bytes app/src/main/res/drawable-hdpi/ic_xlm.png | Bin 0 -> 3919 bytes app/src/main/res/drawable-hdpi/ic_xrp.png | Bin 0 -> 1546 bytes app/src/main/res/drawable-ldpi/ic_binance.png | Bin 0 -> 1692 bytes app/src/main/res/drawable-ldpi/ic_btc.png | Bin 0 -> 1685 bytes .../main/res/drawable-ldpi/ic_btc_cash.png | Bin 0 -> 1790 bytes app/src/main/res/drawable-ldpi/ic_cardano.png | Bin 0 -> 2006 bytes app/src/main/res/drawable-ldpi/ic_eth.png | Bin 0 -> 1669 bytes app/src/main/res/drawable-ldpi/ic_ltc.png | Bin 0 -> 1530 bytes app/src/main/res/drawable-ldpi/ic_rsk.png | Bin 0 -> 1830 bytes app/src/main/res/drawable-ldpi/ic_stellar.png | Bin 0 -> 1715 bytes app/src/main/res/drawable-ldpi/ic_tezos.png | Bin 0 -> 1683 bytes app/src/main/res/drawable-ldpi/ic_xlm.png | Bin 0 -> 1779 bytes app/src/main/res/drawable-ldpi/ic_xrp.png | Bin 0 -> 699 bytes app/src/main/res/drawable-mdpi/ic_binance.png | Bin 0 -> 2305 bytes app/src/main/res/drawable-mdpi/ic_btc.png | Bin 0 -> 2507 bytes .../main/res/drawable-mdpi/ic_btc_cash.png | Bin 0 -> 2460 bytes app/src/main/res/drawable-mdpi/ic_cardano.png | Bin 0 -> 2965 bytes app/src/main/res/drawable-mdpi/ic_eth.png | Bin 0 -> 2330 bytes app/src/main/res/drawable-mdpi/ic_ltc.png | Bin 0 -> 2110 bytes app/src/main/res/drawable-mdpi/ic_rsk.png | Bin 0 -> 2588 bytes app/src/main/res/drawable-mdpi/ic_stellar.png | Bin 0 -> 2435 bytes app/src/main/res/drawable-mdpi/ic_tezos.png | Bin 0 -> 2301 bytes app/src/main/res/drawable-mdpi/ic_xlm.png | Bin 0 -> 2381 bytes app/src/main/res/drawable-mdpi/ic_xrp.png | Bin 0 -> 970 bytes .../main/res/drawable-xhdpi/ic_binance.png | Bin 0 -> 4841 bytes app/src/main/res/drawable-xhdpi/ic_btc.png | Bin 0 -> 6736 bytes .../main/res/drawable-xhdpi/ic_btc_cash.png | Bin 0 -> 5577 bytes .../main/res/drawable-xhdpi/ic_cardano.png | Bin 0 -> 6950 bytes app/src/main/res/drawable-xhdpi/ic_eth.png | Bin 0 -> 5039 bytes app/src/main/res/drawable-xhdpi/ic_ltc.png | Bin 0 -> 4447 bytes app/src/main/res/drawable-xhdpi/ic_rsk.png | Bin 0 -> 6291 bytes .../main/res/drawable-xhdpi/ic_stellar.png | Bin 0 -> 5443 bytes app/src/main/res/drawable-xhdpi/ic_tezos.png | Bin 0 -> 5256 bytes app/src/main/res/drawable-xhdpi/ic_xlm.png | Bin 0 -> 5331 bytes app/src/main/res/drawable-xhdpi/ic_xrp.png | Bin 0 -> 2263 bytes .../main/res/drawable-xxhdpi/ic_binance.png | Bin 0 -> 8173 bytes app/src/main/res/drawable-xxhdpi/ic_btc.png | Bin 0 -> 13647 bytes .../main/res/drawable-xxhdpi/ic_btc_cash.png | Bin 0 -> 8787 bytes .../main/res/drawable-xxhdpi/ic_cardano.png | Bin 0 -> 11381 bytes app/src/main/res/drawable-xxhdpi/ic_eth.png | Bin 0 -> 8125 bytes app/src/main/res/drawable-xxhdpi/ic_ltc.png | Bin 0 -> 7038 bytes app/src/main/res/drawable-xxhdpi/ic_rsk.png | Bin 0 -> 10396 bytes .../main/res/drawable-xxhdpi/ic_stellar.png | Bin 0 -> 9008 bytes app/src/main/res/drawable-xxhdpi/ic_tezos.png | Bin 0 -> 8992 bytes app/src/main/res/drawable-xxhdpi/ic_xlm.png | Bin 0 -> 8286 bytes app/src/main/res/drawable-xxhdpi/ic_xrp.png | Bin 0 -> 3690 bytes .../main/res/drawable-xxxhdpi/ic_binance.png | Bin 0 -> 11090 bytes app/src/main/res/drawable-xxxhdpi/ic_btc.png | Bin 0 -> 21581 bytes .../main/res/drawable-xxxhdpi/ic_btc_cash.png | Bin 0 -> 12192 bytes .../main/res/drawable-xxxhdpi/ic_cardano.png | Bin 0 -> 15649 bytes app/src/main/res/drawable-xxxhdpi/ic_eth.png | Bin 0 -> 11377 bytes app/src/main/res/drawable-xxxhdpi/ic_ltc.png | Bin 0 -> 9667 bytes app/src/main/res/drawable-xxxhdpi/ic_rsk.png | Bin 0 -> 15142 bytes .../main/res/drawable-xxxhdpi/ic_stellar.png | Bin 0 -> 12881 bytes .../main/res/drawable-xxxhdpi/ic_tezos.png | Bin 0 -> 13323 bytes app/src/main/res/drawable-xxxhdpi/ic_xlm.png | Bin 0 -> 11109 bytes app/src/main/res/drawable-xxxhdpi/ic_xrp.png | Bin 0 -> 5289 bytes app/src/main/res/drawable/ic_add.xml | 10 + app/src/main/res/drawable/ic_copy_chip.xml | 9 + app/src/main/res/drawable/ic_inactive.xml | 10 + app/src/main/res/drawable/ic_no_tokens.xml | 468 ++++++++++++++++++ app/src/main/res/drawable/ic_search.xml | 13 + app/src/main/res/drawable/ic_share_chip.xml | 9 + app/src/main/res/drawable/shape_circle.xml | 6 + .../main/res/layout/fragment_add_tokens.xml | 44 ++ app/src/main/res/layout/fragment_details.xml | 56 ++- app/src/main/res/layout/fragment_wallet.xml | 48 ++ .../res/layout/fragment_wallet_details.xml | 127 +++++ .../res/layout/item_currency_subtitle.xml | 23 + .../main/res/layout/item_currency_wallet.xml | 134 +++++ .../main/res/layout/item_popular_token.xml | 96 ++++ .../layout/layout_balance_wallet_details.xml | 136 +++++ .../main/res/layout/layout_search_view.xml | 5 + .../main/res/layout/layout_wallet_details.xml | 175 +++++++ .../res/layout/layout_wallet_long_buttons.xml | 7 +- .../layout/layout_wallet_short_buttons.xml | 12 +- app/src/main/res/menu/popular_tokens.xml | 12 + app/src/main/res/menu/wallet_details.xml | 8 + .../main/res/values/strings_untranslated.xml | 36 ++ 153 files changed, 4393 insertions(+), 1124 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/extensions/AssetManager.kt create mode 100644 app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt create mode 100644 app/src/main/java/com/tangem/tap/common/extensions/String.kt create mode 100644 app/src/main/java/com/tangem/tap/common/extensions/Token.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/TapBlockchainConfig.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/CheckSignedHashesMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TopUpMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TwinsMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TwinsReducer.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt rename app/src/main/java/com/tangem/tap/features/wallet/ui/{ => adapters}/PendingTransactionsAdapter.kt (98%) create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt rename app/src/main/java/com/tangem/tap/features/wallet/ui/{ => adapters}/WarningMessagesAdapter.kt (98%) delete mode 100644 app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/PayIdDialog.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt create mode 100644 app/src/main/res/drawable-hdpi/ic_binance.png create mode 100644 app/src/main/res/drawable-hdpi/ic_btc.png create mode 100644 app/src/main/res/drawable-hdpi/ic_btc_cash.png create mode 100644 app/src/main/res/drawable-hdpi/ic_cardano.png create mode 100644 app/src/main/res/drawable-hdpi/ic_eth.png create mode 100644 app/src/main/res/drawable-hdpi/ic_ltc.png create mode 100644 app/src/main/res/drawable-hdpi/ic_rsk.png create mode 100644 app/src/main/res/drawable-hdpi/ic_stellar.png create mode 100644 app/src/main/res/drawable-hdpi/ic_tezos.png create mode 100644 app/src/main/res/drawable-hdpi/ic_xlm.png create mode 100644 app/src/main/res/drawable-hdpi/ic_xrp.png create mode 100644 app/src/main/res/drawable-ldpi/ic_binance.png create mode 100644 app/src/main/res/drawable-ldpi/ic_btc.png create mode 100644 app/src/main/res/drawable-ldpi/ic_btc_cash.png create mode 100644 app/src/main/res/drawable-ldpi/ic_cardano.png create mode 100644 app/src/main/res/drawable-ldpi/ic_eth.png create mode 100644 app/src/main/res/drawable-ldpi/ic_ltc.png create mode 100644 app/src/main/res/drawable-ldpi/ic_rsk.png create mode 100644 app/src/main/res/drawable-ldpi/ic_stellar.png create mode 100644 app/src/main/res/drawable-ldpi/ic_tezos.png create mode 100644 app/src/main/res/drawable-ldpi/ic_xlm.png create mode 100644 app/src/main/res/drawable-ldpi/ic_xrp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_binance.png create mode 100644 app/src/main/res/drawable-mdpi/ic_btc.png create mode 100644 app/src/main/res/drawable-mdpi/ic_btc_cash.png create mode 100644 app/src/main/res/drawable-mdpi/ic_cardano.png create mode 100644 app/src/main/res/drawable-mdpi/ic_eth.png create mode 100644 app/src/main/res/drawable-mdpi/ic_ltc.png create mode 100644 app/src/main/res/drawable-mdpi/ic_rsk.png create mode 100644 app/src/main/res/drawable-mdpi/ic_stellar.png create mode 100644 app/src/main/res/drawable-mdpi/ic_tezos.png create mode 100644 app/src/main/res/drawable-mdpi/ic_xlm.png create mode 100644 app/src/main/res/drawable-mdpi/ic_xrp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_binance.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_btc.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_btc_cash.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_cardano.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_eth.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_ltc.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_rsk.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_stellar.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_tezos.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_xlm.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_xrp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_binance.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_btc.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_btc_cash.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_cardano.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_eth.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_ltc.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_rsk.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_stellar.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_tezos.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_xlm.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_xrp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_binance.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_btc.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_btc_cash.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_cardano.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_eth.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_ltc.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_rsk.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_stellar.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_tezos.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_xlm.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_xrp.png create mode 100644 app/src/main/res/drawable/ic_add.xml create mode 100644 app/src/main/res/drawable/ic_copy_chip.xml create mode 100644 app/src/main/res/drawable/ic_inactive.xml create mode 100644 app/src/main/res/drawable/ic_no_tokens.xml create mode 100644 app/src/main/res/drawable/ic_search.xml create mode 100644 app/src/main/res/drawable/ic_share_chip.xml create mode 100644 app/src/main/res/drawable/shape_circle.xml create mode 100644 app/src/main/res/layout/fragment_add_tokens.xml create mode 100644 app/src/main/res/layout/fragment_wallet_details.xml create mode 100644 app/src/main/res/layout/item_currency_subtitle.xml create mode 100644 app/src/main/res/layout/item_currency_wallet.xml create mode 100644 app/src/main/res/layout/item_popular_token.xml create mode 100644 app/src/main/res/layout/layout_balance_wallet_details.xml create mode 100644 app/src/main/res/layout/layout_search_view.xml create mode 100644 app/src/main/res/layout/layout_wallet_details.xml create mode 100644 app/src/main/res/menu/popular_tokens.xml create mode 100644 app/src/main/res/menu/wallet_details.xml diff --git a/app/build.gradle b/app/build.gradle index 439820d87c..2c09a36f9c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,10 +68,12 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'com.google.android.material:material:1.2.1' - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' + implementation 'com.google.android.material:material:1.3.0' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + implementation "androidx.core:core-ktx:1.3.2" - implementation 'com.tangem:blockchain:1.141.0' +// implementation 'com.tangem:blockchain:1.141.0' + implementation 'blockchain-sdk-kotlin:blockchain:tangem-20210302.084810-3' implementation 'com.tangem:core:1.101.0' implementation 'com.tangem:sdk:1.101.0' @@ -79,10 +81,10 @@ dependencies { implementation "androidx.browser:browser:1.3.0" //lifecycle - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" - implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.0" + implementation "androidx.lifecycle:lifecycle-common-java8:2.3.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0" implementation "org.rekotlin:rekotlin:1.0.4" diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 6497a63edc..d5f031080b 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -13,6 +13,7 @@ import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager +import com.tangem.tap.domain.tokens.CurrenciesRepository import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.network.createMoshi import com.tangem.tap.persistence.PreferencesStorage @@ -26,6 +27,7 @@ val store = Store( state = AppState() ) lateinit var preferencesStorage: PreferencesStorage +lateinit var currenciesRepository: CurrenciesRepository class TapApplication : Application() { override fun onCreate() { @@ -45,6 +47,7 @@ class TapApplication : Application() { NetworkConnectivity.createInstance(store, this) preferencesStorage = PreferencesStorage(this) PicassoHelper.initPicassoWithCaching(this) + currenciesRepository = CurrenciesRepository(this) loadConfigs() } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/AssetManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/AssetManager.kt new file mode 100644 index 0000000000..b2905ec473 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/AssetManager.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.common.extensions + +import android.content.res.AssetManager + +fun AssetManager.readJsonFileToString(fileName: String): String = + this.open("$fileName.json").bufferedReader().readText() \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt new file mode 100644 index 0000000000..e03e96133b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt @@ -0,0 +1,23 @@ +package com.tangem.tap.common.extensions + +import androidx.annotation.DrawableRes +import com.tangem.blockchain.common.Blockchain +import com.tangem.wallet.R + +@DrawableRes +fun Blockchain.getIconRes(): Int { + return when (this) { + Blockchain.Unknown, Blockchain.Ducatus, Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet, + Blockchain.BinanceTestnet -> 1 + Blockchain.Bitcoin -> R.drawable.ic_btc + Blockchain.BitcoinCash -> R.drawable.ic_btc_cash + Blockchain.Litecoin -> R.drawable.ic_ltc + Blockchain.Ethereum -> R.drawable.ic_eth + Blockchain.RSK -> R.drawable.ic_rsk + Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano + Blockchain.Binance -> R.drawable.ic_binance + Blockchain.Tezos -> R.drawable.ic_tezos + Blockchain.XRP -> R.drawable.ic_xrp + Blockchain.Stellar -> R.drawable.ic_stellar + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 56f0d70061..6e348ea8cf 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -12,6 +12,8 @@ import com.tangem.tap.features.details.ui.twins.TwinWalletWarningFragment import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment import com.tangem.tap.features.home.HomeFragment import com.tangem.tap.features.send.ui.SendFragment +import com.tangem.tap.features.tokens.ui.AddTokensFragment +import com.tangem.tap.features.wallet.ui.WalletDetailsFragment import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.tap.features.wallet.ui.dialogs.TwinsOnboardingFragment import com.tangem.wallet.R @@ -46,5 +48,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.CreateTwinWalletWarning -> TwinWalletWarningFragment() AppScreen.CreateTwinWallet -> CreateTwinWalletFragment() AppScreen.TwinsOnboarding -> TwinsOnboardingFragment() + AppScreen.AddTokens -> AddTokensFragment() + AppScreen.WalletDetails -> WalletDetailsFragment() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt index 42fc659da8..8ed277a345 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt @@ -1,11 +1,5 @@ package com.tangem.tap.common.extensions -import android.graphics.Bitmap -import android.graphics.Color -import com.google.zxing.BarcodeFormat -import com.google.zxing.EncodeHintType -import com.google.zxing.qrcode.QRCodeWriter -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import com.tangem.common.extensions.isZero import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.network.coinmarketcap.FiatCurrency @@ -15,27 +9,6 @@ import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.* - -fun String.toQrCode(): Bitmap { - val hintMap = Hashtable() - hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage - hintMap[EncodeHintType.MARGIN] = 2 - - val qrCodeWriter = QRCodeWriter() - - val size = 256 - - val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, size, size, hintMap) - val width = bitMatrix.width - val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565) - for (x in 0 until width) { - for (y in 0 until width) { - bmp.setPixel(y, x, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE) - } - } - return bmp -} - fun BigDecimal.toFormattedString( decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN, locale: Locale = Locale.US ): String { diff --git a/app/src/main/java/com/tangem/tap/common/extensions/String.kt b/app/src/main/java/com/tangem/tap/common/extensions/String.kt new file mode 100644 index 0000000000..f0f4ef1d00 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/String.kt @@ -0,0 +1,62 @@ +package com.tangem.tap.common.extensions + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Color +import android.text.Spannable +import android.text.style.ForegroundColorSpan +import androidx.core.content.ContextCompat +import androidx.core.text.toSpannable +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.qrcode.QRCodeWriter +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import java.util.* + +fun String?.ellipsizeBeforeSpace(allowedSize: Int): String { + if (this.isNullOrBlank()) return "" + val size = this.length + val sizeDifference = size - allowedSize + val endIndex = this.indexOf(" ") + val startIndex = endIndex - sizeDifference + val newString = this.removeRange(startIndex, endIndex) + return newString.substring(0 until startIndex) + "..." + + newString.substring(startIndex until newString.length) +} + +fun String.colorSegment( + context: Context, + color: Int, + startIndex: Int = 0, + endIndex: Int = this.length +): Spannable { + return this.toSpannable() + .also { spannable -> + spannable.setSpan( + ForegroundColorSpan(ContextCompat.getColor(context, color)), + startIndex, + endIndex, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } +} + +fun String.toQrCode(): Bitmap { + val hintMap = Hashtable() + hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage + hintMap[EncodeHintType.MARGIN] = 2 + + val qrCodeWriter = QRCodeWriter() + + val size = 256 + + val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, size, size, hintMap) + val width = bitMatrix.width + val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565) + for (x in 0 until width) { + for (y in 0 until width) { + bmp.setPixel(y, x, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE) + } + } + return bmp +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt b/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt index 5774e94b61..48ee67afc7 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt @@ -56,4 +56,13 @@ fun TextInputLayout.enableError(enable: Boolean, errorMessage: String? = null) { error = null isErrorEnabled = false } +} + +fun TextView.isEllipsized(): Boolean { + val layout = this.layout + if (layout != null) { + val lines: Int = layout.lineCount + return (lines > 0) && (layout.getEllipsisCount(lines - 1) > 0) + } + return false } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Token.kt b/app/src/main/java/com/tangem/tap/common/extensions/Token.kt new file mode 100644 index 0000000000..f9499f8e70 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/Token.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.common.extensions + +import androidx.annotation.ColorInt +import androidx.core.graphics.toColorInt +import com.tangem.blockchain.common.Token + +@ColorInt +fun Token.getColor(): Int { + return ("#" + this.contractAddress.subSequence(2..7).toString()) + .toColorInt() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt index c7bb0daf0f..a6a0d303ac 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt @@ -8,8 +8,6 @@ import android.content.Intent import android.content.res.Resources import android.graphics.drawable.Drawable import android.os.Build -import android.text.Spannable -import android.text.style.ForegroundColorSpan import android.util.TypedValue import android.view.View import android.view.ViewGroup @@ -19,7 +17,6 @@ import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.content.ContextCompat -import androidx.core.text.toSpannable import androidx.fragment.app.Fragment import com.google.android.material.card.MaterialCardView @@ -106,24 +103,6 @@ fun Activity.setSystemBarTextColor(setTextDark: Boolean) { } } - -fun String.colorSegment( - context: Context, - color: Int, - startIndex: Int = 0, - endIndex: Int = this.length -): Spannable { - return this.toSpannable() - .also { spannable -> - spannable.setSpan( - ForegroundColorSpan(ContextCompat.getColor(context, color)), - startIndex, - endIndex, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } -} - fun View.hideKeyboard() { val inputMethodManager = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? InputMethodManager inputMethodManager?.hideSoftInputFromWindow(this.windowToken, 0) diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 5e80adec68..c60bdaa8f1 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -6,6 +6,7 @@ import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer import com.tangem.tap.features.home.redux.HomeReducer import com.tangem.tap.features.send.redux.reducers.SendScreenReducer +import com.tangem.tap.features.tokens.redux.TokensReducer import com.tangem.tap.features.wallet.redux.WalletReducer import org.rekotlin.Action @@ -20,7 +21,8 @@ fun appReducer(action: Action, state: AppState?): AppState { walletState = WalletReducer.reduce(action, state), sendState = SendScreenReducer.reduce(action, state.sendState), detailsState = DetailsReducer.reduce(action, state), - disclaimerState = DisclaimerReducer.reduce(action, state) + disclaimerState = DisclaimerReducer.reduce(action, state), + tokensState = TokensReducer.reduce(action, state), ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index e60ae97fa7..4548ca046f 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -12,8 +12,10 @@ import com.tangem.tap.features.home.redux.HomeMiddleware import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.send.redux.middlewares.sendMiddleware import com.tangem.tap.features.send.redux.states.SendState -import com.tangem.tap.features.wallet.redux.WalletMiddleware +import com.tangem.tap.features.tokens.redux.TokensMiddleware +import com.tangem.tap.features.tokens.redux.TokensState import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware import org.rekotlin.Middleware import org.rekotlin.StateType @@ -24,7 +26,8 @@ data class AppState( val walletState: WalletState = WalletState(), val sendState: SendState = SendState(), val detailsState: DetailsState = DetailsState(), - val disclaimerState: DisclaimerState = DisclaimerState() + val disclaimerState: DisclaimerState = DisclaimerState(), + val tokensState: TokensState = TokensState(), ) : StateType { companion object { @@ -35,7 +38,8 @@ data class AppState( WalletMiddleware().walletMiddleware, sendMiddleware, DetailsMiddleware().detailsMiddleware, - DisclaimerMiddleware().disclaimerMiddleware + DisclaimerMiddleware().disclaimerMiddleware, + TokensMiddleware().tokensMiddleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index f47245676d..49bc412a5d 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -6,14 +6,10 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tasks.ScanNoteResponse import com.tangem.tap.features.details.redux.SecurityOption import org.rekotlin.Action -import java.math.BigDecimal sealed class GlobalAction : Action { data class SaveScanNoteResponse(val scanNoteResponse: ScanNoteResponse) : GlobalAction() - data class SetFiatRate( - val fiatRates: Pair - ) : GlobalAction() data class ChangeAppCurrency(val appCurrency: FiatCurrencyName) : GlobalAction() object RestoreAppCurrency : GlobalAction() { data class Success(val appCurrency: FiatCurrencyName) : GlobalAction() diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index a0534fe8af..16892e5475 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -23,7 +23,7 @@ val globalMiddleware: Middleware = { dispatch, appState -> if (it.hideWarning(action.warning)) { if (WarningMessagesManager.isAlreadySignedHashesWarning(action.warning)) { //TODO: No appropriate warningMessage identification. Make it better later - store.dispatch(WalletAction.SaveCardId) + store.dispatch(WalletAction.CheckSignedHashes.SaveCardId) } store.dispatch(WalletAction.SetWarnings(it.getWarnings(WarningMessage.Location.MainScreen))) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index afbb6d5b7e..fcd42c9395 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -13,16 +13,11 @@ fun globalReducer(action: Action, state: AppState): GlobalState { return when (action) { is GlobalAction.SaveScanNoteResponse -> globalState.copy(scanNoteResponse = action.scanNoteResponse) - is GlobalAction.SetFiatRate -> { - val rates = globalState.conversionRates.rates.toMutableMap() - rates[action.fiatRates.first] = action.fiatRates.second - globalState.copy(conversionRates = ConversionRates(rates)) - } is GlobalAction.ChangeAppCurrency -> { - globalState.copy(appCurrency = action.appCurrency, conversionRates = ConversionRates(mapOf())) + globalState.copy(appCurrency = action.appCurrency) } is GlobalAction.RestoreAppCurrency.Success -> { - globalState.copy(appCurrency = action.appCurrency, conversionRates = ConversionRates(mapOf())) + globalState.copy(appCurrency = action.appCurrency) } is GlobalAction.UpdateWalletSignedHashes -> { val card = globalState.scanNoteResponse?.card?.copy( diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 2ee123a725..4d96b02a3c 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -9,7 +9,6 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tasks.ScanNoteResponse import com.tangem.tap.network.coinmarketcap.CoinMarketCapService import org.rekotlin.StateType -import java.math.BigDecimal data class GlobalState( val scanNoteResponse: ScanNoteResponse? = null, @@ -17,21 +16,11 @@ data class GlobalState( val payIdManager: PayIdManager = PayIdManager(), val coinMarketCapService: CoinMarketCapService = CoinMarketCapService(), val tangemService: TangemService = TangemService(), - val conversionRates: ConversionRates = ConversionRates(emptyMap()), val configManager: ConfigManager? = null, val warningManager: WarningMessagesManager? = null, val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY ) : StateType -data class ConversionRates( - val rates: Map, -) { - - fun getRate(cryptoCurrency: CryptoCurrencyName): BigDecimal? { - return rates[cryptoCurrency] - } -} - typealias CryptoCurrencyName = String typealias FiatCurrencyName = String diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt index 393a4e7e77..26344b0175 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt @@ -10,6 +10,6 @@ data class NavigationState( ) : StateType enum class AppScreen { - Home, Wallet, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer, - CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding + Home, Wallet, WalletDetails, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer, + CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding, AddTokens } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt b/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt index 3773bd41b7..a6e329ca04 100644 --- a/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt @@ -2,46 +2,13 @@ package com.tangem.tap.domain import com.tangem.blockchain.common.Blockchain import com.tangem.commands.common.network.Result -import com.tangem.tap.network.payid.PayIdService import com.tangem.tap.network.payid.PayIdVerifyService -import com.tangem.tap.network.payid.SetPayIdResponse import com.tangem.tap.network.payid.VerifyPayIdResponse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import retrofit2.HttpException import java.util.* class PayIdManager { - private val payIdService = PayIdService() - - suspend fun getPayId(cardId: String, publicKey: String): Result = withContext(Dispatchers.IO) { - val result = payIdService.getPayId(cardId, publicKey) - when (result) { - is Result.Success -> return@withContext Result.Success(result.data.payId) - is Result.Failure -> { - (result.error as? HttpException)?.let { - if (it.code() == 404) return@withContext Result.Success(null) - } - return@withContext result - } - } - } - - - suspend fun setPayId( - cardId: String, publicKey: String, payId: String, address: String, blockchain: Blockchain - ): Result = withContext(Dispatchers.IO) { - val result = payIdService.setPayId(cardId, publicKey, payId, address, blockchain.getPayIdNetwork()) - when (result) { - is Result.Success -> return@withContext result - is Result.Failure -> { - (result.error as? HttpException)?.let { - if (it.code() == 409) return@withContext Result.Failure(TapError.PayIdAlreadyCreated) - } - return@withContext result - } - } - } suspend fun verifyPayId(payId: String, blockchain: Blockchain): Result = withContext(Dispatchers.IO) { val splitPayId = payId.split("\$") diff --git a/app/src/main/java/com/tangem/tap/domain/TapBlockchainConfig.kt b/app/src/main/java/com/tangem/tap/domain/TapBlockchainConfig.kt new file mode 100644 index 0000000000..b9ec4705a5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/TapBlockchainConfig.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.domain + +class TapBlockchainConfig { + companion object { + val blockchairApiKey: String = "A___0Shpsu4KagE7oSabrw20DfXAqWlT" + val blockcypherTokens: Set = setOf( + "aa8184b0e0894b88a5688e01b3dc1e82", + "56c4ca23c6484c8f8864c32fde4def8d", + "66a8a37c5e9d4d2c9bb191acfe7f93aa" + ) + val infuraProjectId: String = "613a0b14833145968b1f656240c7d245" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 8ffe1480c6..d62759f6f4 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -1,23 +1,22 @@ package com.tangem.tap.domain -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.* import com.tangem.commands.common.card.CardStatus import com.tangem.commands.common.network.Result -import com.tangem.common.extensions.toHexString import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.currenciesRepository import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.extensions.amountToCreateAccount import com.tangem.tap.domain.extensions.isNoAccountError import com.tangem.tap.domain.tasks.ScanNoteResponse +import com.tangem.tap.domain.tasks.isMultiwalletAllowed import com.tangem.tap.domain.twins.TwinsHelper import com.tangem.tap.domain.twins.isTwinCard +import com.tangem.tap.features.tokens.redux.TokensAction import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.network.coinmarketcap.CoinMarketCapService @@ -28,29 +27,25 @@ import java.math.BigDecimal class TapWalletManager { - private val payIdManager = PayIdManager() private val coinMarketCapService = CoinMarketCapService() - suspend fun loadWalletData() { - val walletManager = store.state.globalState.scanNoteResponse?.walletManager - if (walletManager == null) { - store.dispatch(WalletAction.LoadWallet.Failure()) - return + private val blockchainSdkConfig by lazy { + store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig() + } + private val walletManagerFactory: WalletManagerFactory + by lazy { WalletManagerFactory(blockchainSdkConfig) } + + suspend fun loadWalletData(walletManager: WalletManager) { + val result = try { + walletManager.update() + Result.Success(walletManager.wallet) + } catch (exception: Exception) { + Result.Failure(exception) } - loadWallet(walletManager) + handleUpdateWalletResult(result, walletManager) } - suspend fun loadPayId() { - val result = loadPayIdIfNeeded() - result?.let { handlePayIdResult(it) } - } - - suspend fun updateWallet() { - val walletManager = store.state.globalState.scanNoteResponse?.walletManager - if (walletManager == null) { - withContext(Dispatchers.Main) { store.dispatch(WalletAction.UpdateWallet.Failure()) } - return - } + suspend fun updateWallet(walletManager: WalletManager) { val result = try { walletManager.update() Result.Success(walletManager.wallet) @@ -68,12 +63,18 @@ class TapWalletManager { } - suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName) { - val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet ?: return - + suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) { val currencyList = wallet.getTokens().map { it.symbol }.toMutableList() currencyList.add(wallet.blockchain.currency) + loadFiatRate(fiatCurrency, currencyList) + } + suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, cryptoCurrencyName: CryptoCurrencyName) { + val currencyList = listOf(cryptoCurrencyName) + loadFiatRate(fiatCurrency, currencyList) + } + + private suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, currencyList: List) { val results = mutableListOf?>>() currencyList.forEach { results.add(it to coinMarketCapService.getRate(it, fiatCurrency)) } handleFiatRatesResult(results) @@ -88,22 +89,20 @@ class TapWalletManager { store.state.globalState.warningManager?.setBlockchain(data.walletManager?.wallet?.blockchain) val configManager = store.state.globalState.configManager if (TapWorkarounds.isStart2Coin) { - configManager?.turnOff(ConfigManager.isWalletPayIdEnabled) configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled) configManager?.turnOff(ConfigManager.isTopUpEnabled) } else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin - || data.card.cardData?.blockchainName == Blockchain.Bitcoin.id){ - configManager?.turnOff(ConfigManager.isWalletPayIdEnabled) + || data.card.cardData?.blockchainName == Blockchain.Bitcoin.id) { configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled) configManager?.resetToDefault(ConfigManager.isTopUpEnabled) } else { - configManager?.resetToDefault(ConfigManager.isWalletPayIdEnabled) configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled) configManager?.resetToDefault(ConfigManager.isTopUpEnabled) } withContext(Dispatchers.Main) { store.dispatch(WalletAction.ResetState) store.dispatch(GlobalAction.SaveScanNoteResponse(data)) + store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed)) if (data.card.isTwinCard()) { val secondCardId = TwinsHelper.getTwinsCardId(data.card.cardId) val cardNumber = TwinsHelper.getTwinCardNumber(data.card.cardId) @@ -113,13 +112,17 @@ class TapWalletManager { )) } } + if (data.walletManager?.wallet?.blockchain == Blockchain.Ethereum || + data.walletManager?.wallet?.blockchain == Blockchain.EthereumTestnet) { + store.dispatch(TokensAction.LoadCardTokens) + } loadData(data) } } suspend fun loadData(data: ScanNoteResponse) { withContext(Dispatchers.Main) { - store.dispatch(WalletAction.CheckIfWarningNeeded) + store.dispatch(WalletAction.CheckSignedHashes.CheckIfWarningNeeded) val artworkId = data.verifyResponse?.artworkInfo?.id if (data.walletManager != null) { if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { @@ -128,11 +131,40 @@ class TapWalletManager { } val config = store.state.globalState.configManager?.config ?: return@withContext - store.dispatch(WalletAction.LoadWallet( - data.walletManager.wallet, data.verifyResponse?.artworkInfo?.id, config.isTopUpEnabled)) + val primaryWalletManager = data.walletManager + val primaryBlockchain = listOf(data.walletManager.wallet.blockchain) + val primaryToken = primaryWalletManager.presetTokens.toList() + + store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(primaryBlockchain[0])) + if (primaryWalletManager.presetTokens.isNotEmpty()) { + store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken.first())) + } + + if (data.card.isMultiwalletAllowed) { + val savedCurrencies = currenciesRepository.loadCardCurrencies(data.card.cardId) + val tokens = if (savedCurrencies.tokens.isNotEmpty()) { + primaryToken + savedCurrencies.tokens + } else { + primaryToken + } + + val walletManagers = listOf(primaryWalletManager) + + currenciesRepository.getBlockchains() + .filterNot { it == primaryWalletManager.wallet.blockchain } + .mapNotNull { walletManagerFactory.makeWalletManager(data.card, it) } + val otherBlockhains = savedCurrencies.blockchains + + store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers)) + store.dispatch(WalletAction.MultiWallet.AddBlockchains(primaryBlockchain + otherBlockhains)) + store.dispatch(WalletAction.MultiWallet.AddTokens(tokens)) + } else { + store.dispatch(WalletAction.MultiWallet.AddWalletManagers(listOf(primaryWalletManager))) + store.dispatch(WalletAction.MultiWallet.AddBlockchains(primaryBlockchain)) + } + store.dispatch(WalletAction.SetArtworkId(data.verifyResponse?.artworkInfo?.id)) + store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled)) store.dispatch(WalletAction.LoadArtwork(data.card, artworkId)) - store.dispatch(WalletAction.LoadFiatRate) - store.dispatch(WalletAction.LoadPayId) + store.dispatch(WalletAction.LoadFiatRate()) } else if (data.card.status == CardStatus.Empty || data.card.isTwinCard()) { store.dispatch(WalletAction.EmptyWallet) store.dispatch(WalletAction.LoadArtwork(data.card, artworkId)) @@ -143,14 +175,22 @@ class TapWalletManager { } } - private suspend fun loadWallet(walletManager: WalletManager) { - val result = try { - walletManager.update() - Result.Success(walletManager.wallet) - } catch (exception: Exception) { - Result.Failure(exception) + suspend fun reloadData(data: ScanNoteResponse) { + withContext(Dispatchers.Main) { + if (data.walletManager != null) { + if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { + store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection)) + return@withContext + } + val config = store.state.globalState.configManager?.config ?: return@withContext + store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled)) + store.dispatch(WalletAction.LoadFiatRate()) + } else if (data.card.status == CardStatus.Empty || data.card.isTwinCard()) { + store.dispatch(WalletAction.EmptyWallet) + } else { + store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain)) + } } - handleUpdateWalletResult(result, walletManager) } private suspend fun handleUpdateWalletResult(result: Result, walletManager: WalletManager) { @@ -164,59 +204,24 @@ class TapWalletManager { val token = walletManager.wallet.getFirstToken() val amountToCreateAccount = blockchain.amountToCreateAccount(token) if (amountToCreateAccount != null) { - store.dispatch(WalletAction.LoadWallet.NoAccount(amountToCreateAccount.toString())) + store.dispatch(WalletAction.LoadWallet.NoAccount( + walletManager.wallet, + amountToCreateAccount.toString())) return@withContext } } - store.dispatch(WalletAction.LoadWallet.Failure(result.error?.localizedMessage)) + store.dispatch(WalletAction.LoadWallet.Failure(walletManager.wallet, result.error?.localizedMessage)) } } } } - - private suspend fun loadPayIdIfNeeded(): Result? { - val scanNoteResponse = store.state.globalState.scanNoteResponse - if (store.state.globalState.configManager?.config?.isWalletPayIdEnabled == false - || scanNoteResponse?.walletManager?.wallet?.blockchain?.isPayIdSupported() == false) { - return null - } - val cardId = scanNoteResponse?.card?.cardId - val publicKey = scanNoteResponse?.card?.cardPublicKey - if (cardId == null || publicKey == null) { - return null - } - return payIdManager.getPayId(cardId, publicKey.toHexString()) - } - - private suspend fun handlePayIdResult(result: Result) { - withContext(Dispatchers.Main) { - when (result) { - is Result.Success -> { - val config = store.state.globalState.configManager?.config - if (config?.isWalletPayIdEnabled == false) { - store.dispatch(WalletAction.DisablePayId) - return@withContext - } - val payId = result.data - if (payId == null) { - store.dispatch(WalletAction.LoadPayId.NotCreated) - } else { - store.dispatch(WalletAction.LoadPayId.Success(payId)) - } - } - is Result.Failure -> store.dispatch(WalletAction.LoadPayId.Failure) - } - } - } - private suspend fun handleFiatRatesResult(results: List?>>) { withContext(Dispatchers.Main) { results.map { when (it.second) { is Result.Success -> { val rate = it.first to (it.second as Result.Success).data - store.dispatch(GlobalAction.SetFiatRate(rate)) store.dispatch(WalletAction.LoadFiatRate.Success(rate)) } is Result.Failure -> store.dispatch(WalletAction.LoadFiatRate.Failure) diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt index 67025b175d..b1ccd85929 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain.configurable.config +import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.tangem_sdk_new.ui.animation.VoidCallback import com.tangem.tap.domain.configurable.Loader @@ -10,7 +11,7 @@ data class Config( val coinMarketCapKey: String = "f6622117-c043-47a0-8975-9d673ce484de", val moonPayApiKey: String = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE", val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C", - val isWalletPayIdEnabled: Boolean = true, + val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(), val isSendingToPayIdEnabled: Boolean = true, val isTopUpEnabled: Boolean = false, val isCreatingTwinCardsAllowed: Boolean = false @@ -39,7 +40,6 @@ class ConfigManager( fun turnOff(name: String) { when (name) { - isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = false) isSendingToPayIdEnabled -> config = config.copy(isSendingToPayIdEnabled = false) isTopUpEnabled -> config = config.copy(isTopUpEnabled = false) isCreatingTwinCardsAllowed -> config = config.copy(isCreatingTwinCardsAllowed = false) @@ -48,7 +48,6 @@ class ConfigManager( fun resetToDefault(name: String) { when (name) { - isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = defaultConfig.isWalletPayIdEnabled) isSendingToPayIdEnabled -> config = config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled) isTopUpEnabled -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled) isCreatingTwinCardsAllowed -> config = @@ -60,13 +59,11 @@ class ConfigManager( val model = featureModel ?: return config = config.copy( - isWalletPayIdEnabled = model.isWalletPayIdEnabled, isTopUpEnabled = model.isTopUpEnabled, isSendingToPayIdEnabled = model.isSendingToPayIdEnabled, isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed ) defaultConfig = defaultConfig.copy( - isWalletPayIdEnabled = model.isWalletPayIdEnabled, isTopUpEnabled = model.isTopUpEnabled, isSendingToPayIdEnabled = model.isSendingToPayIdEnabled, isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed @@ -78,22 +75,28 @@ class ConfigManager( config = config.copy( coinMarketCapKey = values.coinMarketCapKey, moonPayApiKey = values.moonPayApiKey, - moonPayApiSecretKey = values.moonPayApiSecretKey + moonPayApiSecretKey = values.moonPayApiSecretKey, + blockchainSdkConfig = BlockchainSdkConfig( + blockchairApiKey = values.blockchairApiKey, + blockcypherTokens = values.blockcypherTokens, + infuraProjectId = values.infuraProjectId + ) ) defaultConfig = defaultConfig.copy( coinMarketCapKey = values.coinMarketCapKey, moonPayApiKey = values.moonPayApiKey, - moonPayApiSecretKey = values.moonPayApiSecretKey + moonPayApiSecretKey = values.moonPayApiSecretKey, + blockchainSdkConfig = BlockchainSdkConfig( + blockchairApiKey = values.blockchairApiKey, + blockcypherTokens = values.blockcypherTokens, + infuraProjectId = values.infuraProjectId + ) ) } companion object { - const val isWalletPayIdEnabled = "isWalletPayIdEnabled" const val isSendingToPayIdEnabled = "isSendingToPayIdEnabled" const val isCreatingTwinCardsAllowed = "isCreatingTwinCardsAllowed" const val isTopUpEnabled = "isTopUpEnabled" - const val coinMarketCapKey = "coinMarketCapKey" - const val moonPayApiKey = "moonPayApiKey" - const val moonPayApiSecretKey = "moonPayApiSecretKey" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt index 32ce46da8b..69c5f0a8b0 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt @@ -15,6 +15,9 @@ class ConfigValueModel( val coinMarketCapKey: String, val moonPayApiKey: String, val moonPayApiSecretKey: String, + val blockchairApiKey: String?, + val blockcypherTokens: Set?, + val infuraProjectId: String?, ) class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) { diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt index 66c4238850..3ceeaadda6 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt @@ -4,32 +4,41 @@ import com.tangem.CardSession import com.tangem.CardSessionRunnable import com.tangem.TangemError import com.tangem.TangemSdkError +import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.commands.CommandResponse import com.tangem.commands.ReadIssuerDataCommand import com.tangem.commands.common.card.Card import com.tangem.commands.common.card.CardStatus +import com.tangem.commands.common.card.EllipticCurve import com.tangem.commands.common.card.masks.Product import com.tangem.commands.verifycard.VerifyCardCommand import com.tangem.commands.verifycard.VerifyCardResponse import com.tangem.common.CompletionResult import com.tangem.common.extensions.toHexString import com.tangem.tap.domain.TapSdkError +import com.tangem.tap.domain.TapWorkarounds import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.domain.twins.isTwinCard +import com.tangem.tap.store import com.tangem.tasks.ScanTask data class ScanNoteResponse( val walletManager: WalletManager?, val card: Card, val verifyResponse: VerifyCardResponse? = null, - val secondTwinPublicKey: String? = null + val secondTwinPublicKey: String? = null, + val multiwalletCard: Boolean = false, ) : CommandResponse class ScanNoteTask(val card: Card? = null) : CardSessionRunnable { override val requiresPin2 = false + private val blockchainSdkConfig = store.state.globalState.configManager?.config + ?.blockchainSdkConfig ?: BlockchainSdkConfig() + private val walletManagerFactory = WalletManagerFactory(blockchainSdkConfig) + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { ScanTask().run(session) { result -> when (result) { @@ -53,7 +62,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable { callback(CompletionResult.Success(ScanNoteResponse( - walletManager, card, verifyResult.data, publicKey + walletManager, card, verifyResult.data, publicKey, + batchesAllowingMultiwallet.contains(card.cardData?.batchId) ))) } is CompletionResult.Failure -> { @@ -95,7 +105,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable> = moshi.adapter( + Types.newParameterizedType(List::class.java, TokenDao::class.java) + ) + private val blockchainsAdapter: JsonAdapter> = moshi.adapter( + Types.newParameterizedType(List::class.java, Blockchain::class.java) + ) + + fun loadCardCurrencies(cardId: String): CardCurrencies { + return CardCurrencies(loadSavedTokens(cardId), loadSavedBlockchains(cardId)) + } + + fun saveCardCurrencies(cardId: String, currencies: CardCurrencies) { + saveTokens(cardId, currencies.tokens) + saveBlockchains(cardId, currencies.blockchains) + } + + fun saveAddedToken(cardId: String, token: Token) { + val tokens = loadSavedTokens(cardId) + token + saveTokens(cardId, tokens) + } + + fun saveAddedBlockchain(cardId: String, blockchain: Blockchain) { + val blockchains = loadSavedBlockchains(cardId) + blockchain + saveBlockchains(cardId, blockchains) + } + + fun removeToken(cardId: String, token: Token) { + val tokens = loadSavedTokens(cardId).filterNot { it == token } + saveTokens(cardId, tokens) + } + + fun removeBlockchain(cardId: String, blockchain: Blockchain) { + val blockchains = loadSavedBlockchains(cardId).filterNot { it == blockchain } + saveBlockchains(cardId, blockchains) + } + + private fun loadSavedTokens(cardId: String): List { + return try { + val json = context.readFileText(getFileNameForTokens(cardId)) + tokensAdapter.fromJson(json)!!.map { it.toToken() } + } catch (exception: Exception) { + emptyList() + } + } + + private fun saveTokens(cardId: String, tokens: List) { + val json = tokensAdapter.toJson(tokens.map { TokenDao.fromToken(it) }) + context.rewriteFile(json, getFileNameForTokens(cardId)) + } + + private fun loadSavedBlockchains(cardId: String): List { + return try { + val json = context.readFileText(getFileNameForBlockchains(cardId)) + blockchainsAdapter.fromJson(json) ?: emptyList() + } catch (exception: Exception) { + emptyList() + } + } + + private fun saveBlockchains(cardId: String, blockchains: List) { + val json = blockchainsAdapter.toJson(blockchains) + context.rewriteFile(json, getFileNameForBlockchains(cardId)) + } + + private fun Context.readFileText(fileName: String): String = + this.openFileInput(fileName).bufferedReader().readText() + + private fun Context.rewriteFile(content: String, fileName: String) { + this.openFileOutput(fileName, Context.MODE_PRIVATE).use { + it.write(content.toByteArray(), 0, content.length) + } + } + + fun getPopularTokens(): List { + val json = context.assets.readJsonFileToString(POPULAR_TOKENS_FILE_NAME) + return tokensAdapter.fromJson(json)!!.map { it.toToken() } + } + + fun getBlockchains(): List { + return listOf( + Blockchain.Bitcoin, Blockchain.BitcoinCash, Blockchain.Binance, Blockchain.Litecoin, + Blockchain.XRP, Blockchain.Tezos, + Blockchain.Ethereum, Blockchain.RSK) + } + + companion object { + private const val POPULAR_TOKENS_FILE_NAME = "erc20_tokens" + private const val FILE_NAME_PREFIX_TOKENS = "tokens" + private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains" + fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId" + fun getFileNameForBlockchains(cardId: String): String = "${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId" + } +} + + +@JsonClass(generateAdapter = true) +data class TokenDao( + val name: String, + val symbol: String, + val contractAddress: String, + val decimalCount: Int +) { + fun toToken(): Token { + return Token(name = name, symbol = symbol, contractAddress = contractAddress, decimals = decimalCount) + } + + companion object { + fun fromToken(token: Token): TokenDao { + return TokenDao(name = token.name, symbol = token.symbol, contractAddress = token.contractAddress, decimalCount = token.decimals) + + } + } +} + +@JsonClass(generateAdapter = true) +data class CardCurrenciesDao( + val tokens: List, + val blockchains: List +) { + fun toCardCurrencies(): CardCurrencies { + return CardCurrencies(tokens = tokens.map { it.toToken() }, blockchains = blockchains) + } + + companion object { + fun fromCardCurrencies(cardCurrencies: CardCurrencies): CardCurrenciesDao { + return CardCurrenciesDao(tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }, blockchains = cardCurrencies.blockchains) + } + } +} + +data class CardCurrencies( + val tokens: List, + val blockchains: List +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 2ec73c0f00..5bb753ff4c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -17,7 +17,7 @@ sealed class DetailsAction : Action { data class PrepareScreen( val card: Card, val scanNoteResponse: ScanNoteResponse, - val wallet: Wallet?, + val wallets: List, val isCreatingTwinWalletAllowed: Boolean?, val fiatCurrencyName: FiatCurrencyName, val fiatCurrencies: List? = null, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 95a0a3952c..b66aa958e9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -104,7 +104,7 @@ class DetailsMiddleware { is DetailsAction.AppCurrencyAction.SelectAppCurrency -> { preferencesStorage.saveAppCurrency(action.fiatCurrencyName) store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrencyName)) - store.dispatch(WalletAction.LoadFiatRate) + store.dispatch(WalletAction.LoadFiatRate()) } } } @@ -152,9 +152,7 @@ class DetailsMiddleware { store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure) } } - } - } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 805d31f8c9..9c90579d69 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -66,14 +66,15 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta } else { null } + return DetailsState( - card = action.card, wallet = action.wallet, + card = action.card, wallets = action.wallets, cardInfo = action.card.toCardInfo(), appCurrencyState = AppCurrencyState( action.fiatCurrencyName ), securityScreenState = SecurityScreenState(currentOption = securityOption), - createTwinWalletState = twinsState + createTwinWalletState = twinsState, ) } @@ -81,8 +82,9 @@ private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsS return when (action) { DetailsAction.EraseWallet.Check -> { val notAllowedByCard = state.card?.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true - val notEmpty = state.wallet?.recentTransactions?.isNullOrEmpty() != true || - state.wallet.amounts.toSendableAmounts().isNotEmpty() + val notEmpty = state.wallets.any { + !it.recentTransactions.isNullOrEmpty() || it.amounts.toSendableAmounts().isNotEmpty() + } val eraseWalletState = when { notAllowedByCard -> EraseWalletState.NotAllowedByCard notEmpty -> EraseWalletState.NotEmpty diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 4730507ebc..f4d58c3000 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -12,7 +12,7 @@ import java.util.* data class DetailsState( val card: Card? = null, - val wallet: Wallet? = null, + val wallets: List = emptyList(), val cardInfo: CardInfo? = null, val appCurrencyState: AppCurrencyState = AppCurrencyState(), val eraseWalletState: EraseWalletState? = null, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletMiddleware.kt index 38cfac39bc..d40bb904d7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletMiddleware.kt @@ -18,7 +18,7 @@ class CreateTwinWalletMiddleware { fun handle(action: DetailsAction.CreateTwinWalletAction) { when (action) { is DetailsAction.CreateTwinWalletAction.ShowWarning -> { - val wallet = store.state.detailsState.wallet + val wallet = store.state.detailsState.wallets.getOrNull(0) if (wallet == null) { store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning)) return diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt index 262482b360..50de2e5535 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt @@ -77,7 +77,6 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber { when (result.error) { @@ -142,7 +150,7 @@ private fun sendTransaction( } // make it easier latter by handling an appropriate enumError or, like on iOS, // accept a string identifier of the error message - message.contains("Target account is not created. To create account send 1+ XLM.")-> { + message.contains("Target account is not created. To create account send 1+ XLM.") -> { dispatch(SendAction.SendError(TapError.XmlError.AssetAccountNotCreated)) } else -> { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt index 0d816d6d62..4144059510 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt @@ -63,12 +63,10 @@ private class PrepareSendScreenStatesReducer : SendInternalReducer { val amountToExtract = prepareAction.tokenAmount ?: prepareAction.coinAmount!! val decimals = amountToExtract.decimals - val coinConverter = createCurrencyConverter(walletManager.wallet.blockchain.currency, decimals) - val tokenConverter = createCurrencyConverter(prepareAction.tokenAmount?.currencySymbol ?: "", decimals) return sendState.copy( walletManager = walletManager, - coinConverter = coinConverter, - tokenConverter = tokenConverter, + coinConverter = action.coinRate?.let { CurrencyConverter(it, decimals) }, + tokenConverter = action.tokenRate?. let { CurrencyConverter(it, decimals) }, amountState = sendState.amountState.copy( amountToExtract = amountToExtract, typeOfAmount = amountToExtract.type, @@ -79,11 +77,6 @@ private class PrepareSendScreenStatesReducer : SendInternalReducer { ) ) } - - private fun createCurrencyConverter(currency: String, decimals: Int): CurrencyConverter? { - val rate = store.state.globalState.conversionRates.getRate(currency) - return if (rate == null) null else CurrencyConverter(rate, decimals) - } } internal fun updateLastState(sendState: SendState, lastChangedState: IdStateHolder): SendState { diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index ca47b5613e..cba07cdeb5 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -34,8 +34,8 @@ import com.tangem.tap.features.send.redux.FeeActionUi.* import com.tangem.tap.features.send.redux.states.FeeType import com.tangem.tap.features.send.redux.states.MainCurrencyType import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber -import com.tangem.tap.features.wallet.ui.SpacesItemDecoration -import com.tangem.tap.features.wallet.ui.WarningMessagesAdapter +import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration +import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.mainScope import com.tangem.tap.store import com.tangem.wallet.R diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index 20d3988515..8084b7e5d5 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -22,7 +22,7 @@ import com.tangem.tap.features.send.redux.states.* import com.tangem.tap.features.send.ui.FeeUiHelper import com.tangem.tap.features.send.ui.SendFragment import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog -import com.tangem.tap.features.wallet.ui.WarningMessagesAdapter +import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.btn_expand_collapse.* diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt new file mode 100644 index 0000000000..e0c9abe119 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.features.tokens.redux + +import com.tangem.blockchain.common.Token +import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem +import com.tangem.tap.features.wallet.redux.WalletData +import org.rekotlin.Action + +sealed class TokensAction : Action { + + object LoadCurrencies : TokensAction() { + data class Success(val currencies: List) : TokensAction() + } + + object LoadCardTokens : TokensAction() { + data class Success(val tokens: List) : TokensAction() + } + + data class SetAddedCurrencies(val wallets: List) : TokensAction() + + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt new file mode 100644 index 0000000000..7923d17bd4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -0,0 +1,28 @@ +package com.tangem.tap.features.tokens.redux + +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.currenciesRepository +import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem +import com.tangem.tap.store +import org.rekotlin.Middleware + +class TokensMiddleware { + + val tokensMiddleware: Middleware = { dispatch, state -> + { next -> + { action -> + when (action) { + is TokensAction.LoadCurrencies -> { + val tokens = currenciesRepository.getPopularTokens() + val blockchains = currenciesRepository.getBlockchains() + val currencies = CurrencyListItem.createListOfCurrencies( + blockchains, tokens + ) + store.dispatch(TokensAction.LoadCurrencies.Success(currencies)) + } + } + next(action) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt new file mode 100644 index 0000000000..e0c5600ad1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.features.tokens.redux + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Action + +class TokensReducer { + companion object { + fun reduce(action: Action, state: AppState): TokensState = internalReduce(action, state) + } +} + +private fun internalReduce(action: Action, state: AppState): TokensState { + if (action !is TokensAction) return state.tokensState + + val tokensState = state.tokensState + return when (action) { + is TokensAction.LoadCurrencies.Success -> { + tokensState.copy(currencies = action.currencies) + } + is TokensAction.SetAddedCurrencies -> { + tokensState.copy(addedCurrencies = action.wallets.mapNotNull { it.currencyData.currencySymbol }) + } + is TokensAction.LoadCardTokens.Success -> { + tokensState.copy(addedTokens = LinkedHashSet( + action.tokens.map { TokenWithAmount(it, null) } + )) + } + else -> tokensState + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt new file mode 100644 index 0000000000..5169e63b96 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.features.tokens.redux + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Token +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem +import org.rekotlin.StateType + +data class TokensState( + val addedTokens: LinkedHashSet = LinkedHashSet(), + val addedCurrencies: List = emptyList(), + val currencies: List = emptyList(), +) : StateType + +data class TokenWithAmount( + val token: Token, + val amount: Amount? = null +) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt new file mode 100644 index 0000000000..e3d8eb80fb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt @@ -0,0 +1,119 @@ +package com.tangem.tap.features.tokens.ui + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.transition.TransitionInflater +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.tokens.redux.TokensState +import com.tangem.tap.features.tokens.ui.adapters.CurrenciesAdapter +import com.tangem.tap.mainScope +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.fragment_add_tokens.* +import kotlinx.android.synthetic.main.fragment_details_twin_cards.toolbar +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.* +import org.rekotlin.StoreSubscriber + + +class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), + StoreSubscriber { + + private lateinit var viewAdapter: CurrenciesAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + store.dispatch(NavigationAction.PopBackTo()) + } + }) + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(R.transition.slide_right) + exitTransition = inflater.inflateTransition(R.transition.fade) + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.tokensState == newState.tokensState + }.select { it.tokensState } + } + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + toolbar.setNavigationOnClickListener { activity?.onBackPressed() } + setupPopularTokensRecyclerView() + } + + + private fun setupPopularTokensRecyclerView() { + viewAdapter = CurrenciesAdapter() + rv_popular_tokens.layoutManager = LinearLayoutManager(context) + rv_popular_tokens.adapter = viewAdapter + } + + + override fun newState(state: TokensState) { + if (activity == null) return + viewAdapter.addedCurrencies = state.addedCurrencies + viewAdapter.submitUnfilteredList(state.currencies) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.menu_search -> { + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.popular_tokens, menu) + + val menuItem = menu.findItem(R.id.menu_search) + val searchView: SearchView = menuItem.actionView as SearchView + searchView.queryHint = "Type here to search" + searchView.maxWidth = android.R.attr.width + searchView.inputtedTextAsFlow() + .debounce(400) + .distinctUntilChanged() + .onEach { + viewAdapter.filter(searchView.query) + } + .launchIn(mainScope) + return super.onCreateOptionsMenu(menu, inflater); + } +} + +fun SearchView.inputtedTextAsFlow(): Flow = callbackFlow { + val watcher = setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + offer(newText ?: "") + return false + } + }) + awaitClose { (watcher) } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt new file mode 100644 index 0000000000..9fe1ba05bd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt @@ -0,0 +1,165 @@ +package com.tangem.tap.features.tokens.ui.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.item_currency_subtitle.view.* +import kotlinx.android.synthetic.main.item_popular_token.view.* +import java.util.* + +class CurrenciesAdapter : ListAdapter(DiffUtilCallback) { + + var addedCurrencies: List = emptyList() + + private var unfilteredList = listOf() + + fun submitUnfilteredList(list: List) { + unfilteredList = list + submitList(list) + } + + override fun getItemViewType(position: Int): Int { + return when (currentList[position]) { + is CurrencyListItem.TitleListItem -> 0 + is CurrencyListItem.BlockchainListItem, is CurrencyListItem.TokenListItem -> 1 + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + 0 -> TitleViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_currency_subtitle, parent, false)) + 1 -> CurrenciesViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_popular_token, parent, false)) + else -> CurrenciesViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_popular_token, parent, false)) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val listItem = currentList[position] + if (holder is TitleViewHolder && listItem is CurrencyListItem.TitleListItem) { + holder.bind(listItem) + } else if (holder is CurrenciesViewHolder) { + holder.bind(listItem, addedCurrencies) + } + } + + object DiffUtilCallback : DiffUtil.ItemCallback() { + override fun areContentsTheSame( + oldItem: CurrencyListItem, newItem: CurrencyListItem + ) = oldItem == newItem + + override fun areItemsTheSame( + oldItem: CurrencyListItem, newItem: CurrencyListItem + ) = oldItem == newItem + } + + fun filter(query: CharSequence?) { + val list = mutableListOf() + + if (!query.isNullOrEmpty()) { + val queryNormalized = query.toString().toLowerCase(Locale.US) + list.addAll( + unfilteredList.filter { element -> + when (element) { + is CurrencyListItem.BlockchainListItem -> { + element.blockchain.currency.toLowerCase(Locale.US).contains(queryNormalized) + || element.blockchain.fullName.toLowerCase(Locale.US).contains(queryNormalized) + + } + is CurrencyListItem.TokenListItem -> { + element.token.name.toLowerCase(Locale.US).contains(queryNormalized) + || element.token.symbol.toLowerCase(Locale.US).contains(queryNormalized) + } + else -> false + } + }) + } else { + list.addAll(unfilteredList) + } + submitList(list) + } + + class CurrenciesViewHolder(val view: View) : + RecyclerView.ViewHolder(view) { + fun bind(currency: CurrencyListItem, addedCurrencies: List) { + when (currency) { + is CurrencyListItem.BlockchainListItem -> { + val blockchain = currency.blockchain + view.tv_currency_name.text = blockchain.fullName + view.tv_currency_symbol.text = blockchain.currency + val isAdded = addedCurrencies.any { it == blockchain.currency } + view.btn_add_token.show(!isAdded) + view.btn_token_added.show(isAdded) + + view.iv_currency.setImageResource(currency.blockchain.getIconRes()) + view.iv_currency.colorFilter = null + view.tv_token_letter.text = null + + view.btn_add_token.setOnClickListener { + store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchain)) + view.btn_add_token.hide() + view.btn_token_added.show() + } + } + is CurrencyListItem.TokenListItem -> { + val token = currency.token + view.tv_currency_name.text = token.name + view.tv_currency_symbol.text = token.symbol + + val isAdded = addedCurrencies.any { it == token.symbol } + view.btn_add_token.show(!isAdded) + view.btn_token_added.show(isAdded) + + view.iv_currency.setImageResource(R.drawable.shape_circle) + view.iv_currency.setColorFilter(token.getColor()) + view.tv_token_letter.text = token.symbol.take(1) + + view.btn_add_token.setOnClickListener { + store.dispatch(WalletAction.MultiWallet.AddToken(token)) + view.btn_add_token.hide() + view.btn_token_added.show() + } + } + } + } + } + + class TitleViewHolder(val view: View) : + RecyclerView.ViewHolder(view) { + fun bind(title: CurrencyListItem.TitleListItem) { + view.tv_subtitle.text = view.getString(title.titleResId).toUpperCase(Locale.US) + } + } +} + + +sealed class CurrencyListItem { + data class TokenListItem(val token: Token) : CurrencyListItem() + data class BlockchainListItem(val blockchain: Blockchain) : CurrencyListItem() + data class TitleListItem(@StringRes val titleResId: Int) : CurrencyListItem() + + companion object { + fun createListOfCurrencies(blockchains: List, tokens: List): List { + val blockchainsTitle = R.string.add_tokens_subtitle_blockchains + val tokensTitle = R.string.add_tokens_subtitle_tokens + return listOf(TitleListItem(blockchainsTitle)) + + blockchains.map { BlockchainListItem(it) } + + listOf(TitleListItem(tokensTitle)) + + tokens.map { TokenListItem(it) } + } + } +} + diff --git a/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt b/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt index 21cdc9d649..0ade915979 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.wallet.models +import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionStatus import com.tangem.tap.common.extensions.toFormattedString @@ -47,4 +48,13 @@ fun List.toPendingTransactions(walletAddress: String): List.removeUnknownTransactions(): List { return this.filter { it.type != PendingTransactionType.Unknown } +} + +fun TransactionData.toPendingTransactionForToken(token: Token, walletAddress: String): PendingTransaction? { + if (this.amount.currencySymbol != token.symbol) return null + return this.toPendingTransaction(walletAddress) +} + +fun List.toPendingTransactionsForToken(token: Token, walletAddress: String): List { + return this.mapNotNull { it.toPendingTransactionForToken(token, walletAddress) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index 5f11e932bf..222d18a108 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -1,11 +1,9 @@ package com.tangem.tap.features.wallet.redux import android.content.Context -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.AddressType import com.tangem.commands.common.card.Card -import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.domain.TapError @@ -23,73 +21,94 @@ sealed class WalletAction : Action { data class Failure(val error: TapError) : WalletAction() } - data class LoadWallet( - val wallet: Wallet, val artworkId: String?, val allowTopUp: Boolean - ) : WalletAction() { + + data class LoadWallet(val allowTopUp: Boolean? = null, val currency: CryptoCurrencyName? = null) : WalletAction() { data class Success(val wallet: Wallet) : WalletAction() - data class NoAccount(val amountToCreateAccount: String) : WalletAction() - data class Failure(val errorMessage: String? = null) : WalletAction() + data class NoAccount(val wallet: Wallet, val amountToCreateAccount: String) : WalletAction() + data class Failure(val wallet: Wallet, val errorMessage: String? = null) : WalletAction() } - object CheckIfWarningNeeded : WalletAction() - object CheckHashesCountOnline : WalletAction() - object NeedToCheckHashesCountOnline : WalletAction() - object ConfirmHashesCount : WalletAction() - data class SetWarnings(val warningList: List) : WalletAction() - object SaveCardId : WalletAction() + data class SetArtworkId(val artworkId: String?) : WalletAction() - object UpdateWallet : WalletAction() { +// sealed class ProcessWallet : WalletAction() { +// data class LoadWallet(val artworkId: String?, val allowTopUp: Boolean +// ) : ProcessWallet() { +// data class Success(val wallet: Wallet) : ProcessWallet() +// data class NoAccount(val wallet: Wallet, val amountToCreateAccount: String) : ProcessWallet() +// data class Failure(val wallet: Wallet, val errorMessage: String? = null) : ProcessWallet() +// } +// +// data class UpdateWallet(val currency: CryptoCurrencyName? = null) : ProcessWallet() { +// object ScheduleUpdatingWallet : ProcessWallet() +// data class Success(val wallet: Wallet) : ProcessWallet() +// data class Failure(val errorMessage: String? = null) : ProcessWallet() +// } +// } + + + sealed class MultiWallet : WalletAction() { + data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet() + data class AddWalletManagers(val walletManagers: List) : MultiWallet() + data class AddBlockchain(val blockchain: Blockchain) : MultiWallet() + data class AddBlockchains(val blockchains: List) : MultiWallet() + data class AddTokens(val tokens: List) : MultiWallet() + data class AddToken(val token: Token) : MultiWallet() + data class TokenLoaded(val amount: Amount) : MultiWallet() + data class SelectWallet(val walletData: WalletData?) : MultiWallet() + data class RemoveWallet(val walletData: WalletData) : MultiWallet() + data class SetPrimaryBlockchain(val blockchain: Blockchain) : MultiWallet() + data class SetPrimaryToken(val token: Token) : MultiWallet() + } + + sealed class CheckSignedHashes : WalletAction() { + object CheckIfWarningNeeded : CheckSignedHashes() + object CheckHashesCountOnline : CheckSignedHashes() + object NeedToCheckHashesCountOnline : CheckSignedHashes() + object ConfirmHashesCount : CheckSignedHashes() + object SaveCardId : CheckSignedHashes() + } + + data class SetWarnings(val warningList: List) : WalletAction() + + data class UpdateWallet(val currency: CryptoCurrencyName? = null) : WalletAction() { object ScheduleUpdatingWallet : WalletAction() data class Success(val wallet: Wallet) : WalletAction() data class Failure(val errorMessage: String? = null) : WalletAction() } - object LoadFiatRate : WalletAction() { - data class Success(val fiatRates: Pair) : WalletAction() + data class LoadFiatRate( + val wallet: Wallet? = null, val currency: CryptoCurrencyName? = null + ) : WalletAction() { + data class Success(val fiatRate: Pair) : WalletAction() object Failure : WalletAction() } - object LoadPayId : WalletAction() { - data class Success(val payId: String) : WalletAction() - object NotCreated : WalletAction() - object Failure : WalletAction() - } - - object DisablePayId : WalletAction() - data class LoadArtwork(val card: Card, val artworkId: String?) : WalletAction() { data class Success(val artwork: Artwork) : WalletAction() object Failure : WalletAction() } - object Scan : WalletAction() - data class Send(val amount: Amount? = null) : WalletAction() { - data class ChooseCurrency(val amounts: List?) : WalletAction() - object Cancel : WalletAction() - } - object CreatePayId : WalletAction() { - data class CompleteCreatingPayId(val payId: String) : WalletAction() - data class Success(val payId: String) : WalletAction() - object EmptyField : WalletAction(), ErrorAction { - override val error = TapError.PayIdEmptyField - } - - class Failure(override val error: TapError) : WalletAction(), ErrorAction - object Cancel : WalletAction() - } - - data class CopyAddress(val context: Context) : WalletAction() { + data class CopyAddress(val address: String, val context: Context) : WalletAction() { object Success : WalletAction(), NotificationAction { override val messageResource = R.string.wallet_notification_address_copied } } + data class ShareAddress(val address: String, val context: Context) : WalletAction() + object ShowQrCode : WalletAction() object HideDialog : WalletAction() - data class ExploreAddress(val context: Context) : WalletAction() + data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction() + object CreateWallet : WalletAction() object EmptyWallet : WalletAction() + object Scan : WalletAction() + + data class Send(val amount: Amount? = null) : WalletAction() { + data class ChooseCurrency(val amounts: List?) : WalletAction() + object Cancel : WalletAction() + } sealed class TopUpAction : WalletAction() { data class TopUp(val context: Context, val toolbarColor: Int) : TopUpAction() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt deleted file mode 100644 index a06b52a6b1..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt +++ /dev/null @@ -1,316 +0,0 @@ -package com.tangem.tap.features.wallet.redux - -import android.content.Intent -import android.net.Uri -import androidx.browser.customtabs.CustomTabsIntent -import androidx.core.content.ContextCompat -import com.tangem.blockchain.common.* -import com.tangem.blockchain.extensions.SimpleResult -import com.tangem.commands.common.card.Card -import com.tangem.commands.common.card.CardType -import com.tangem.commands.common.network.Result -import com.tangem.common.CompletionResult -import com.tangem.common.extensions.getType -import com.tangem.common.extensions.toHexString -import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler -import com.tangem.tap.common.extensions.copyToClipboard -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.navigation.AppScreen -import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.PayIdManager -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.TopUpHelper -import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager -import com.tangem.tap.domain.extensions.toSendableAmounts -import com.tangem.tap.domain.twins.TwinsHelper -import com.tangem.tap.domain.twins.isTwinCard -import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.features.details.redux.twins.CreateTwinWallet -import com.tangem.tap.features.send.redux.PrepareSendScreen -import com.tangem.tap.features.wallet.models.toPendingTransactions -import com.tangem.tap.network.NetworkConnectivity -import com.tangem.tap.network.NetworkStateChanged -import com.tangem.tap.preferencesStorage -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.rekotlin.Action -import org.rekotlin.Middleware - -class WalletMiddleware { - private val topUpMiddleware = TopUpMiddleware() - - val walletMiddleware: Middleware = { dispatch, state -> - { next -> - { action -> - when (action) { - is WalletAction.TopUpAction -> topUpMiddleware.handle(action) - is WalletAction.LoadWallet -> { - scope.launch { - store.state.globalState.tapWalletManager.loadWalletData() - } - } - is WalletAction.LoadPayId -> { - scope.launch { - store.state.globalState.tapWalletManager.loadPayId() - } - } - is WalletAction.LoadFiatRate -> { - scope.launch { - store.state.globalState.tapWalletManager.loadFiatRate(store.state.globalState.appCurrency) - } - } - is WalletAction.CreateWallet -> { - if (store.state.walletState.twinCardsState != null) { - store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning( - store.state.globalState.scanNoteResponse?.card?.cardId?.let { - TwinsHelper.getTwinCardNumber(it) - }, - CreateTwinWallet.CreateWallet - )) - } else { - scope.launch { - val result = tangemSdkManager.createWallet( - store.state.globalState.scanNoteResponse?.card?.cardId - ) - when (result) { - is CompletionResult.Success -> { - store.state.globalState.tapWalletManager - .onCardScanned(result.data) - } - - } - } - } - } - is WalletAction.UpdateWallet -> { - if (store.state.walletState.state == ProgressState.Done) { - scope.launch { store.state.globalState.tapWalletManager.updateWallet() } - } - } - is WalletAction.UpdateWallet.Success -> setupWalletUpdate(action.wallet) - is WalletAction.LoadWallet.Success -> { - store.dispatch(WalletAction.CheckHashesCountOnline) - if (!store.state.walletState.updatingWallet) setupWalletUpdate(action.wallet) - } - is WalletAction.CreatePayId.CompleteCreatingPayId -> { - scope.launch { - val cardId = store.state.globalState.scanNoteResponse?.card?.cardId - val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet - val publicKey = store.state.globalState.scanNoteResponse?.card?.cardPublicKey - if (cardId != null && wallet != null && publicKey != null) { - val result = PayIdManager().setPayId( - cardId, publicKey.toHexString(), - action.payId, wallet.address, wallet.blockchain - ) - withContext(Dispatchers.Main) { - when (result) { - is Result.Success -> - store.dispatch(WalletAction.CreatePayId.Success(action.payId)) - is Result.Failure -> { - val error = result.error as? TapError - ?: TapError.PayIdCreatingError - store.dispatch(WalletAction.CreatePayId.Failure(error)) - } - } - } - } - } - } - is WalletAction.Scan -> { - scope.launch { - val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler) - when (result) { - is CompletionResult.Success -> { - tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card) - store.state.globalState.tapWalletManager - .onCardScanned(result.data, true) - if (store.state.walletState.twinCardsState != null) { - val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown() - if (showOnboarding) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding)) - } - } - } - } - } - } - is WalletAction.LoadData -> { - scope.launch { - store.state.globalState.scanNoteResponse?.let { - store.state.globalState.tapWalletManager.loadData(it) - } - } - } - is NetworkStateChanged -> { - store.state.globalState.scanNoteResponse?.let { scanNoteResponse -> - store.dispatch(WalletAction.CheckHashesCountOnline) - scope.launch { - store.state.globalState.tapWalletManager.onCardScanned(scanNoteResponse) - } - } - } - is WalletAction.CopyAddress -> { - store.state.walletState.walletAddresses?.selectedAddress?.address?.let { - action.context.copyToClipboard(it) - store.dispatch(WalletAction.CopyAddress.Success) - } - } - is WalletAction.ExploreAddress -> { - val uri = Uri.parse(store.state.walletState.walletAddresses?.selectedAddress?.exploreUrl) - val intent = Intent(Intent.ACTION_VIEW, uri) - ContextCompat.startActivity(action.context, intent, null) - } - is WalletAction.Send -> { - val newAction = prepareSendAction(action.amount) - store.dispatch(newAction) - if (newAction is PrepareSendScreen) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.Send)) - } - } - is WalletAction.CheckIfWarningNeeded -> { - val globalState = store.state.globalState - val validator = globalState.scanNoteResponse?.walletManager as? SignatureCountValidator - globalState.scanNoteResponse?.card?.let { card -> - store.state.globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local) - if (card.getType() != CardType.Release) addWarningMessage(WarningMessagesManager.devCardWarning()) - if (!preferencesStorage.wasCardScannedBefore(card.cardId)) { - checkIfWarningNeeded(card, validator)?.let { addWarningMessage(it) } - } - updateWarningMessages() - } - - } - is WalletAction.CheckHashesCountOnline -> checkHashesCountOnline() - is WalletAction.SaveCardId -> { - val cardId = store.state.globalState.scanNoteResponse?.card?.cardId - cardId?.let { preferencesStorage.saveScannedCardId(it) } - } - is WalletAction.TwinsAction.SetTwinCard -> { - val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown() - if (showOnboarding) store.dispatch(WalletAction.TwinsAction.ShowOnboarding) - } - is WalletAction.TwinsAction.SetOnboardingShown -> { - preferencesStorage.saveTwinsOnboardingShown() - } - } - next(action) - } - } - } - - private fun setupWalletUpdate(wallet: Wallet) { - if (!wallet.recentTransactions.toPendingTransactions(wallet.address).isNullOrEmpty()) { - store.dispatch(WalletAction.UpdateWallet.ScheduleUpdatingWallet) - scope.launch(Dispatchers.IO) { - delay(10000) - withContext(Dispatchers.Main) { - store.dispatch(WalletAction.UpdateWallet) - } - } - } - } - - - private fun prepareSendAction(amount: Amount?): Action { - return if (amount != null) { - if (amount.type is AmountType.Token) { - PrepareSendScreen(store.state.walletState.wallet?.amounts?.get(AmountType.Coin), amount) - } else { - PrepareSendScreen(amount) - } - } else { - val amounts = store.state.walletState.wallet?.amounts?.toSendableAmounts() - if (amounts?.size ?: 0 > 1) { - WalletAction.Send.ChooseCurrency(amounts) - } else { - val amountToSend = amounts?.first() - PrepareSendScreen(amountToSend) - } - } - } - - private fun checkIfWarningNeeded( - card: Card, signatureCountValidator: SignatureCountValidator? = null, - ): WarningMessage? { - if (card.isTwinCard()) return null - - return if (signatureCountValidator == null) { - if (card.walletSignedHashes ?: 0 > 0) { - WarningMessagesManager.alreadySignedHashesWarning() - } else { - store.dispatch(WalletAction.SaveCardId) - null - } - } else { - store.dispatch(WalletAction.NeedToCheckHashesCountOnline) - null - } - } - - private fun checkHashesCountOnline() { - if (store.state.walletState.hashesCountVerified != false) return - if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return - - val card = store.state.globalState.scanNoteResponse?.card - if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) return - - if (card.isTwinCard()) return - - val validator = store.state.globalState.scanNoteResponse?.walletManager - as? SignatureCountValidator - scope.launch { - val result = validator?.validateSignatureCount(card.walletSignedHashes ?: 0) - withContext(Dispatchers.Main) { - when (result) { - SimpleResult.Success -> { - store.dispatch(WalletAction.ConfirmHashesCount) - store.dispatch(WalletAction.SaveCardId) - } - is SimpleResult.Failure -> - if (result.error is BlockchainSdkError.SignatureCountNotMatched) { - addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true) - } else if (card.walletSignedHashes ?: 0 > 0) { - addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true) - } - } - } - } - } - - private fun addWarningMessage(warning: WarningMessage, autoUpdate: Boolean = false) { - store.state.globalState.warningManager?.addWarning(warning) - if (autoUpdate) updateWarningMessages() - } - - private fun updateWarningMessages() { - val warningManager = store.state.globalState.warningManager ?: return - store.dispatch(WalletAction.SetWarnings(warningManager.getWarnings(WarningMessage.Location.MainScreen))) - } -} - -private class TopUpMiddleware { - fun handle(action: WalletAction.TopUpAction) { - when (action) { - is WalletAction.TopUpAction.TopUp -> { - val config = store.state.globalState.configManager?.config ?: return - val defaultAddress = store.state.walletState.walletAddresses!!.list[0].address - val url = TopUpHelper.getUrl( - store.state.walletState.currencyData.currencySymbol!!, - defaultAddress, - config.moonPayApiKey, - config.moonPayApiSecretKey - ) - val customTabsIntent = CustomTabsIntent.Builder() - .setToolbarColor(action.toolbarColor) - .build() - customTabsIntent.launchUrl(action.context, Uri.parse(url)); - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt index 275ad57895..a75c65a889 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt @@ -1,25 +1,26 @@ package com.tangem.tap.features.wallet.redux import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Wallet import com.tangem.commands.common.network.TangemService -import com.tangem.common.extensions.isZero import com.tangem.common.extensions.toHexString import com.tangem.tap.common.extensions.toFiatString import com.tangem.tap.common.extensions.toFormattedCurrencyString -import com.tangem.tap.common.extensions.toFormattedString import com.tangem.tap.common.extensions.toQrCode import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.domain.TapError import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.twins.TwinCardNumber -import com.tangem.tap.features.wallet.models.removeUnknownTransactions -import com.tangem.tap.features.wallet.models.toPendingTransactions +import com.tangem.tap.features.wallet.redux.reducers.MultiWalletReducer +import com.tangem.tap.features.wallet.redux.reducers.OnWalletLoadedReducer +import com.tangem.tap.features.wallet.redux.reducers.TwinsReducer import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData -import com.tangem.tap.features.wallet.ui.TokenData -import com.tangem.tap.store import org.rekotlin.Action +import java.math.BigDecimal class WalletReducer { companion object { @@ -29,11 +30,19 @@ class WalletReducer { private fun internalReduce(action: Action, state: AppState): WalletState { + val multiWalletReducer = MultiWalletReducer() + val twinsReducer = TwinsReducer() + val onWalletLoadedReducer = OnWalletLoadedReducer() + if (action !is WalletAction) return state.walletState var newState = state.walletState when (action) { + is WalletAction.CheckSignedHashes -> newState = handleCheckSignedHashesActions(action, newState) + is WalletAction.TwinsAction -> newState = twinsReducer.reduce(action, newState) + is WalletAction.MultiWallet -> newState = multiWalletReducer.reduce(action, newState) + is WalletAction.ResetState -> newState = WalletState() is WalletAction.EmptyWallet -> { val creatingWalletAllowed = !(newState.twinCardsState != null && @@ -41,121 +50,149 @@ private fun internalReduce(action: Action, state: AppState): WalletState { newState = newState.copy( state = ProgressState.Done, - currencyData = BalanceWidgetData(BalanceStatus.EmptyCard), - mainButton = WalletMainButton.CreateWalletButton(creatingWalletAllowed), - topUpState = TopUpState(false) - ) + wallets = listOf(WalletData( + currencyData = BalanceWidgetData(BalanceStatus.EmptyCard), + mainButton = WalletMainButton.CreateWalletButton(creatingWalletAllowed), + topUpState = TopUpState(false) + ))) } is WalletAction.LoadData.Failure -> { when (action.error) { is TapError.NoInternetConnection -> { - val wallet = state.globalState.scanNoteResponse?.walletManager?.wallet newState = newState.copy( state = ProgressState.Error, error = ErrorType.NoInternetConnection, - walletAddresses = createAddressList(wallet, newState.walletAddresses), - currencyData = BalanceWidgetData( - status = BalanceStatus.Unreachable, - currency = wallet?.blockchain?.fullName, - token = wallet?.getFirstToken()?.symbol?.let { - TokenData("", tokenSymbol = it) - }), - mainButton = WalletMainButton.SendButton(false), - topUpState = TopUpState(false) ) } is TapError.UnknownBlockchain -> { newState = newState.copy( state = ProgressState.Done, - currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain), - topUpState = TopUpState(false) + wallets = listOf(WalletData( + currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain), + topUpState = TopUpState(false) + )) ) } } + } + is WalletAction.LoadData -> { + newState = newState.copy( + state = ProgressState.Loading, + error = null, + ) } is WalletAction.LoadWallet -> { - val wallet = action.wallet + if (action.currency == null) { + val wallets = newState.wallets.map { wallet -> + wallet.copy( + currencyData = wallet.currencyData.copy( + status = BalanceStatus.Loading, + currency = wallet.currencyData.currency, + currencySymbol = wallet.currencyData.currencySymbol, + ), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = action.allowTopUp + ?: wallet.topUpState.allowed) + ) + } + newState = newState.copy( + state = ProgressState.Loading, + wallets = wallets + ) + } else { + val walletManager = newState.getWalletManager(action.currency) ?: return newState + val currencies = listOf(walletManager.wallet.blockchain.currency + walletManager.presetTokens.map { it.symbol }) + val newWallets = newState.wallets.filter { currencies.contains(it.currencyData.currencySymbol) } + .map { wallet -> + wallet.copy( + currencyData = wallet.currencyData.copy( + status = BalanceStatus.Loading, + currency = wallet.currencyData.currency, + currencySymbol = wallet.currencyData.currencySymbol, + ), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState( + allowed = action.allowTopUp ?: wallet.topUpState.allowed + ) + ) + } + val wallets = newState.replaceSomeWallets(newWallets) + newState = newState.copy(wallets = wallets) + } + + } + is WalletAction.LoadWallet.Success -> newState = onWalletLoadedReducer.reduce(action.wallet, newState) + is WalletAction.UpdateWallet.Success -> { + newState = onWalletLoadedReducer.reduce(action.wallet, newState) +// newState = newState.copy(updatingWallet = newState.pendingTransactions.isNotEmpty()) + } + + is WalletAction.LoadWallet.NoAccount -> { + val walletData = newState.getWalletData(action.wallet.blockchain.currency)?.copy( + currencyData = BalanceWidgetData( + BalanceStatus.NoAccount, action.wallet.blockchain.fullName, + currencySymbol = action.wallet.blockchain.currency, + amountToCreateAccount = action.amountToCreateAccount + ) + ) + val wallets = newState.replaceWalletInWallets(walletData) + val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) { + ProgressState.Loading + } else { + ProgressState.Done + } + newState = newState.copy( + state = state, + wallets = wallets + + ) + } + is WalletAction.LoadWallet.Failure -> { + val walletData = newState.getWalletData(action.wallet.blockchain.currency) + val newWalletData = walletData?.copy( + currencyData = walletData.currencyData.copy( + status = BalanceStatus.Unreachable, + errorMessage = action.errorMessage + ), + topUpState = TopUpState(false) + ) + val wallets = newState.replaceWalletInWallets(newWalletData) + val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) { + ProgressState.Loading + } else { + ProgressState.Done + } + newState = newState.copy( + state = state, wallets = wallets + ) + } + is WalletAction.SetArtworkId -> { val cardImage = if (newState.cardImage?.artworkId == action.artworkId) { newState.cardImage } else { null } - newState = newState.copy( - state = ProgressState.Loading, - cardImage = cardImage, - currencyData = BalanceWidgetData( - BalanceStatus.Loading, - wallet.blockchain.fullName, - currencySymbol = wallet.blockchain.currency, - token = wallet.getFirstToken()?.symbol?.let { - TokenData("", tokenSymbol = it) - } - ), - walletAddresses = createAddressList(wallet, newState.walletAddresses), - mainButton = WalletMainButton.SendButton(false), - topUpState = TopUpState(allowed = action.allowTopUp) - ) + newState = newState.copy(cardImage = cardImage) } - is WalletAction.LoadWallet.Success -> newState = onWalletLoaded(action.wallet, newState) - is WalletAction.LoadWallet.NoAccount -> { - val wallet = state.globalState.scanNoteResponse?.walletManager?.wallet - newState = newState.copy( - state = ProgressState.Done, wallet = wallet, - currencyData = BalanceWidgetData( - BalanceStatus.NoAccount, wallet?.blockchain?.fullName, - currencySymbol = wallet?.blockchain?.currency, - amountToCreateAccount = action.amountToCreateAccount - ) - ) - } - is WalletAction.LoadWallet.Failure -> newState = newState.copy( - state = ProgressState.Done, - currencyData = newState.currencyData.copy( - status = BalanceStatus.Unreachable, - errorMessage = action.errorMessage - ), - topUpState = TopUpState(false) - ) - is WalletAction.UpdateWallet -> { - if (store.state.walletState.state == ProgressState.Done) { - newState = newState.copy(updatingWallet = true) - } - } - is WalletAction.UpdateWallet.ScheduleUpdatingWallet -> - newState = newState.copy(updatingWallet = true) - is WalletAction.UpdateWallet.Success -> { - newState = onWalletLoaded(action.wallet, newState) - newState = newState.copy(updatingWallet = newState.pendingTransactions.isNotEmpty()) - } - is WalletAction.UpdateWallet.Failure -> newState = newState.copy(updatingWallet = false) - is WalletAction.LoadFiatRate -> { - newState.copy(currencyData = newState.currencyData.copy( - fiatAmount = null, - token = newState.currencyData.token?.copy(fiatAmount = null)) - ) - } - is WalletAction.LoadFiatRate.Success -> { - val rate = action.fiatRates.second ?: return newState - val currency = action.fiatRates.first - val fiatAmount = if (currency == newState.wallet?.blockchain?.currency) { - newState.wallet?.amounts?.get(AmountType.Coin)?.value - ?.toFiatString(rate, state.globalState.appCurrency) - } else { - newState.currencyData.fiatAmount - } - val token = newState.wallet?.getFirstToken() - val tokenFiatAmount = if (currency == token?.symbol) { - newState.wallet?.getTokenAmount(token)?.value?.toFiatString(rate, state.globalState.appCurrency) - } else { - newState.currencyData.token?.fiatAmount - } - newState = newState.copy(currencyData = newState.currencyData.copy( - fiatAmount = fiatAmount, - token = newState.currencyData.token?.copy(fiatAmount = tokenFiatAmount) - )) - } +// is WalletAction.UpdateWallet -> { +// if (store.state.walletState.state == ProgressState.Done) { +// newState = newState.copy(updatingWallet = true) +// } +// } +// is WalletAction.UpdateWallet.ScheduleUpdatingWallet -> +// newState = newState.copy(updatingWallet = true) + +// is WalletAction.UpdateWallet.Failure -> newState = newState.copy(updatingWallet = false) +// is WalletAction.LoadFiatRate -> { +// newState.copy(currencyData = newState.currencyData.copy( +// fiatAmount = null, +// token = newState.currencyData.token?.copy(fiatAmount = null)) +// ) +// } + is WalletAction.LoadFiatRate.Success -> + newState = setNewFiatRate(action.fiatRate, state.globalState.appCurrency, newState) is WalletAction.LoadArtwork -> { val cardId = action.card.cardId val cardPublicKey = action.card.cardPublicKey?.toHexString() @@ -177,38 +214,18 @@ private fun internalReduce(action: Action, state: AppState): WalletState { newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl)) } is WalletAction.ShowQrCode -> { + val selectedWalletData = newState.getWalletData(newState.selectedWallet) newState = newState.copy( walletDialog = WalletDialog.QrDialog( - newState.walletAddresses?.selectedAddress?.shareUrl?.toQrCode(), - newState.walletAddresses?.selectedAddress?.shareUrl, - newState.currencyData.currency + selectedWalletData?.walletAddresses?.selectedAddress?.shareUrl?.toQrCode(), + selectedWalletData?.walletAddresses?.selectedAddress?.shareUrl, + selectedWalletData?.currencyData?.currency ) ) } is WalletAction.HideDialog -> { newState = newState.copy(walletDialog = null) } - is WalletAction.LoadPayId.Success -> newState = newState.copy( - payIdData = PayIdData(PayIdState.Created, action.payId) - ) - is WalletAction.LoadPayId.NotCreated -> newState = newState.copy( - payIdData = PayIdData(PayIdState.NotCreated, null) - ) - is WalletAction.DisablePayId -> newState = newState.copy( - payIdData = PayIdData(PayIdState.Disabled, null) - ) - is WalletAction.CreatePayId, is WalletAction.CreatePayId.Failure -> - newState = newState.copy( - walletDialog = WalletDialog.CreatePayIdDialog(CreatingPayIdState.EnterPayId) - ) - is WalletAction.CreatePayId.CompleteCreatingPayId -> newState = newState.copy( - walletDialog = WalletDialog.CreatePayIdDialog(CreatingPayIdState.Waiting) - ) - is WalletAction.CreatePayId.Success -> newState = newState.copy( - payIdData = PayIdData(PayIdState.Created, action.payId), - walletDialog = null - ) - is WalletAction.CreatePayId.Cancel -> newState = newState.copy(walletDialog = null) is WalletAction.Send.ChooseCurrency -> { newState = newState.copy( walletDialog = WalletDialog.SelectAmountToSendDialog(action.amounts) @@ -216,43 +233,18 @@ private fun internalReduce(action: Action, state: AppState): WalletState { } is WalletAction.Send.Cancel -> newState = newState.copy(walletDialog = null) is WalletAction.SetWarnings -> newState = newState.copy(mainWarningsList = action.warningList) - is WalletAction.NeedToCheckHashesCountOnline -> - newState = newState.copy(hashesCountVerified = false) - is WalletAction.ConfirmHashesCount -> - newState = newState.copy(hashesCountVerified = true) - is WalletAction.TopUpAction -> { - newState = newState.copy(topUpState = handleTopUpActions(action, newState.topUpState)) - } + is WalletAction.TopUpAction -> return newState is WalletAction.ChangeSelectedAddress -> { - val walletAddresses = newState.walletAddresses ?: return newState + val selectedWalletData = newState.getWalletData(newState.selectedWallet) + + val walletAddresses = newState.getWalletData(selectedWalletData?.currencyData?.currencySymbol)?.walletAddresses + ?: return newState val address = walletAddresses.list.firstOrNull { it.type == action.type } ?: return newState - - newState = newState.copy(walletAddresses = WalletAddresses(address, walletAddresses.list)) - } - is WalletAction.TwinsAction.SetTwinCard -> { - newState = newState.copy( - twinCardsState = TwinCardsState( - secondCardId = action.secondCardId, - cardNumber = action.number, - showTwinOnboarding = newState.twinCardsState?.showTwinOnboarding - ?: false, - isCreatingTwinCardsAllowed = action.isCreatingTwinCardsAllowed - ) - ) - } - is WalletAction.TwinsAction.ShowOnboarding -> { - newState = newState.copy( - twinCardsState = newState.twinCardsState?.copy(showTwinOnboarding = true) - ?: TwinCardsState(null, null, - showTwinOnboarding = true, - isCreatingTwinCardsAllowed = false) - ) - } - is WalletAction.TwinsAction.SetOnboardingShown -> { - newState = newState.copy( - twinCardsState = newState.twinCardsState?.copy(showTwinOnboarding = false) + val wallets = newState.replaceWalletInWallets( + selectedWalletData?.copy(walletAddresses = WalletAddresses(address, walletAddresses.list)) ) + newState = newState.copy(wallets = wallets) } } return newState @@ -281,56 +273,89 @@ fun createAddressList(wallet: Wallet?, walletAddresses: WalletAddresses? = null) return WalletAddresses(listOfAddressData[indexOfSelectedWallet], listOfAddressData) } -private fun handleTopUpActions(action: WalletAction.TopUpAction, state: TopUpState): TopUpState { +private fun handleCheckSignedHashesActions(action: WalletAction.CheckSignedHashes, state: WalletState): WalletState { return when (action) { - is WalletAction.TopUpAction.TopUp -> state + WalletAction.CheckSignedHashes.CheckHashesCountOnline -> state + WalletAction.CheckSignedHashes.CheckIfWarningNeeded -> state + WalletAction.CheckSignedHashes.ConfirmHashesCount -> state.copy(hashesCountVerified = true) + WalletAction.CheckSignedHashes.NeedToCheckHashesCountOnline -> state.copy(hashesCountVerified = false) + WalletAction.CheckSignedHashes.SaveCardId -> state } } -private fun onWalletLoaded( - wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null + +private fun setNewFiatRate( + fiatRate: Pair, + appCurrency: FiatCurrencyName, state: WalletState ): WalletState { - val fiatCurrencySymbol = store.state.globalState.appCurrency + val rate = fiatRate.second ?: return state + val rateFormatted = rate.toFormattedCurrencyString(2, appCurrency) + val currency = fiatRate.first + + if (!state.isMultiwalletAllowed) { + return setSingeWalletFiatRate(rate, rateFormatted, currency, appCurrency, state) + } else { + return setMultiWalletFiatRate(rate, rateFormatted, currency, appCurrency, state) + } +} + +private fun setMultiWalletFiatRate( + rate: BigDecimal, rateFormatted: String, currency: CryptoCurrencyName, + appCurrency: FiatCurrencyName, state: WalletState +): WalletState { + val walletData = state.getWalletData(currency) ?: return state + val wallet = state.walletManagers.find { it.wallet.blockchain.currency == currency }?.wallet + if (wallet != null) { + val newWalletData = state.getWalletData(currency)?.copy( + currencyData = walletData.currencyData.copy( + fiatAmount = wallet.amounts[AmountType.Coin]?.value + ?.toFiatString(rate, appCurrency)), + fiatRate = rate, fiatRateString = rateFormatted + ) + return state.copy(wallets = state.replaceWalletInWallets(newWalletData)) + } else { + val ethereumWallet = state.walletManagers.find { it.wallet.blockchain == Blockchain.Ethereum }?.wallet + val token = ethereumWallet?.getTokens()?.find { it.symbol == currency } ?: return state + val tokenFiatAmount = ethereumWallet.getTokenAmount(token)?.value?.toFiatString(rate, appCurrency) + val newWalletData = state.getWalletData(currency)?.copy( + currencyData = walletData.currencyData.copy( + fiatAmount = tokenFiatAmount), + fiatRate = rate, fiatRateString = rateFormatted + ) + return state.copy(wallets = state.replaceWalletInWallets(newWalletData)) + } +} + +private fun setSingeWalletFiatRate( + rate: BigDecimal, rateFormatted: String, currency: CryptoCurrencyName, + appCurrency: FiatCurrencyName, state: WalletState +): WalletState { + val wallet = state.walletManagers[0].wallet val token = wallet.getFirstToken() - val tokenData = if (token != null) { - val tokenAmount = wallet.getTokenAmount(token) - if (tokenAmount != null) { - val tokenFiatRate = store.state.globalState.conversionRates.getRate(tokenAmount.currencySymbol) - val tokenFiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencySymbol) } - TokenData(tokenAmount.value?.toFormattedString(tokenAmount.decimals) ?: "", - tokenAmount.currencySymbol, tokenFiatAmount) - } else { - null - } - } else { - null - } - val amount = wallet.amounts[AmountType.Coin]?.value - val formattedAmount = amount?.toFormattedCurrencyString( - wallet.blockchain.decimals(), - wallet.blockchain.currency) - val fiatRate = store.state.globalState.conversionRates.getRate(wallet.blockchain.currency) - val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) } - val pendingTransactions = wallet.recentTransactions - .toPendingTransactions(wallet.address) + if (currency == state.primaryWallet?.currencyData?.currencySymbol) { + val fiatAmount = wallet.amounts[AmountType.Coin]?.value + ?.toFiatString(rate, appCurrency) + val walletData = state.primaryWallet.copy( + currencyData = state.primaryWallet.currencyData.copy(fiatAmount = fiatAmount), + fiatRate = rate, + fiatRateString = rateFormatted + ) + return state.copy(wallets = listOf(walletData)) + } else if (currency == token?.symbol) { + val tokenFiatAmount = wallet.getTokenAmount(token)?.value?.toFiatString(rate, appCurrency) + val tokenData = state.primaryWallet?.currencyData?.token?.copy( + fiatAmount = tokenFiatAmount, + fiatRate = rate, + fiatRateString = rateFormatted + ) - val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty() - val balanceStatus = if (pendingTransactions.isNotEmpty()) { - BalanceStatus.TransactionInProgress - } else { - BalanceStatus.VerifiedOnline + val walletData = state.primaryWallet?.copy( + currencyData = state.primaryWallet.currencyData.copy( + token = tokenData + )) + val wallets = walletData?.let { listOf(walletData) } ?: emptyList() + return state.copy(wallets = wallets) } - return walletState.copy( - state = ProgressState.Done, wallet = wallet, - currencyData = BalanceWidgetData( - balanceStatus, wallet.blockchain.fullName, - currencySymbol = wallet.blockchain.currency, - formattedAmount, - token = tokenData, - fiatAmount = fiatAmount - ), - pendingTransactions = pendingTransactions.removeUnknownTransactions(), - mainButton = WalletMainButton.SendButton(sendButtonEnabled) - ) + return state } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index 1b541930c7..2958717c4a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -1,47 +1,98 @@ package com.tangem.tap.features.wallet.redux import android.graphics.Bitmap -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.extensions.isAboveZero import com.tangem.tap.common.entities.Button import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.domain.configurable.warningMessage.WarningMessage +import com.tangem.tap.domain.extensions.toSendableAmounts import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.features.wallet.models.PendingTransaction +import com.tangem.tap.features.wallet.models.toPendingTransactions +import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken import com.tangem.tap.features.wallet.ui.BalanceWidgetData +import com.tangem.tap.store import org.rekotlin.StateType +import java.math.BigDecimal data class WalletState( val state: ProgressState = ProgressState.Done, val error: ErrorType? = null, val cardImage: Artwork? = null, - val wallet: Wallet? = null, - val pendingTransactions: List = emptyList(), val hashesCountVerified: Boolean? = null, - val walletAddresses: WalletAddresses? = null, - val currencyData: BalanceWidgetData = BalanceWidgetData(), - val payIdData: PayIdData = PayIdData(), val walletDialog: WalletDialog? = null, - val updatingWallet: Boolean = false, - val mainButton: WalletMainButton = WalletMainButton.SendButton(false), - val topUpState: TopUpState = TopUpState(), val twinCardsState: TwinCardsState? = null, - val mainWarningsList: List = mutableListOf() + val mainWarningsList: List = mutableListOf(), + val wallets: List = emptyList(), + val walletManagers: List = emptyList(), + val isMultiwalletAllowed: Boolean = false, + val cardCurrency: CryptoCurrencyName? = null, + val selectedWallet: CryptoCurrencyName? = null, + val primaryBlockchain: Blockchain? = null, + val primaryToken: Token? = null ) : StateType { - val showDetails: Boolean = - currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard && - currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.UnknownBlockchain - val showMultipleAddress: Boolean - get() { - val listOfAddresses = walletAddresses?.list ?: return false - return (wallet?.blockchain == Blockchain.Bitcoin || - wallet?.blockchain == Blockchain.BitcoinTestnet || - wallet?.blockchain == Blockchain.CardanoShelley) && - listOfAddresses.size > 1 + val primaryWallet = if (wallets.isNotEmpty()) wallets[0] else null +// val primaryWalletManager = if (walletManagers.isNotEmpty()) walletManagers[0] else null + + val shouldShowDetails: Boolean = + primaryWallet?.currencyData?.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard && + primaryWallet?.currencyData?.status != com.tangem.tap.features.wallet.ui.BalanceStatus.UnknownBlockchain + + fun getWalletManager(currencyName: CryptoCurrencyName?): WalletManager? { + if (currencyName == null) return null + val walletManager = walletManagers.find { it.wallet.blockchain.currency == currencyName } + if (walletManager != null) return walletManager + + val ethereumWalletManager = walletManagers.find { it.wallet.blockchain == Blockchain.Ethereum } + return if (ethereumWalletManager?.presetTokens?.find { it.symbol == currencyName } != null) { + ethereumWalletManager + } else { + null } + } + + fun getWalletData(currencyName: CryptoCurrencyName?): WalletData? { + if (currencyName == null) return null + return wallets.find { it.currencyData.currencySymbol == currencyName } + } + + fun getSelectedWalletData(): WalletData? { + return wallets.find { it.currencyData.currencySymbol == selectedWallet } + } + + fun canBeRemoved(walletData: WalletData?): Boolean { + if (walletData == null) return false + + if (walletData.blockchain != store.state.walletState.primaryBlockchain + && (walletData.token == null || walletData.token != store.state.walletState.primaryToken)) { + val walletManager = getWalletManager(walletData.currencyData.currencySymbol) + ?: return true + val wallet = walletManager.wallet + if (walletData.blockchain != null) { + return wallet.recentTransactions.toPendingTransactions(wallet.address).isEmpty() && + wallet.amounts.toSendableAmounts().isEmpty() + } else if (walletData.token != null) ( + return wallet.recentTransactions.toPendingTransactionsForToken( + walletData.token, wallet.address).isEmpty() + && wallet.amounts[AmountType.Token(token = walletData.token)]?.isAboveZero() != true + ) + } + return false + } + + fun replaceWalletInWallets(walletData: WalletData?): List { + if (walletData == null) return wallets + return wallets.filter { it.currencyData.currency != walletData.currencyData.currency } + walletData + } + + fun replaceSomeWallets(newWallets: List): List { + val currencies = newWallets.map { it.currencyData.currency } + return wallets.filter { !currencies.contains(it.currencyData.currency) } + newWallets + } + } sealed class WalletDialog { @@ -49,24 +100,13 @@ sealed class WalletDialog { val qrCode: Bitmap?, val shareUrl: String?, val currencyName: CryptoCurrencyName? ) : WalletDialog() - data class CreatePayIdDialog(val creatingPayIdState: CreatingPayIdState?) : WalletDialog() data class SelectAmountToSendDialog(val amounts: List?) : WalletDialog() - data class TwinsOnboardingFragment(val secondCardId: String): WalletDialog() } enum class ProgressState { Loading, Done, Error } enum class ErrorType { NoInternetConnection } -enum class PayIdState { Disabled, Loading, NotCreated, Created, ErrorLoading } - -data class PayIdData( - val payIdState: PayIdState = PayIdState.Loading, - val payId: String? = null -) - -enum class CreatingPayIdState { EnterPayId, Waiting } - sealed class WalletMainButton(enabled: Boolean) : Button(enabled) { class SendButton(enabled: Boolean) : WalletMainButton(enabled) class CreateWalletButton(enabled: Boolean) : WalletMainButton(enabled) @@ -81,7 +121,8 @@ data class AddressData( val address: String, val type: AddressType, val shareUrl: String, - val exploreUrl: String + val exploreUrl: String, + val qrCode: Bitmap? = null ) data class Artwork( @@ -110,4 +151,27 @@ data class TwinCardsState( val cardNumber: TwinCardNumber?, val showTwinOnboarding: Boolean, val isCreatingTwinCardsAllowed: Boolean -) \ No newline at end of file +) + +data class WalletData( + val pendingTransactions: List = emptyList(), + val hashesCountVerified: Boolean? = null, + val walletAddresses: WalletAddresses? = null, + val currencyData: BalanceWidgetData = BalanceWidgetData(), + val updatingWallet: Boolean = false, + val topUpState: TopUpState = TopUpState(), + val allowToSend: Boolean = true, + val fiatRateString: String? = null, + val fiatRate: BigDecimal? = null, + val mainButton: WalletMainButton = WalletMainButton.SendButton(false), + val token: Token? = null, + val blockchain: Blockchain? = null, +) { + fun shouldShowMultipleAddress(): Boolean { + val listOfAddresses = walletAddresses?.list ?: return false + return (currencyData.currencySymbol == Blockchain.Bitcoin.currency || + currencyData.currencySymbol == Blockchain.BitcoinTestnet.currency || + currencyData.currencySymbol == Blockchain.CardanoShelley.currency) && + listOfAddresses.size > 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/CheckSignedHashesMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/CheckSignedHashesMiddleware.kt new file mode 100644 index 0000000000..9ae002d4ad --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/CheckSignedHashesMiddleware.kt @@ -0,0 +1,103 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.SignatureCountValidator +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.commands.common.card.Card +import com.tangem.commands.common.card.CardType +import com.tangem.common.extensions.getType +import com.tangem.tap.common.redux.global.GlobalState +import com.tangem.tap.domain.configurable.warningMessage.WarningMessage +import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager +import com.tangem.tap.domain.tasks.isMultiwalletAllowed +import com.tangem.tap.domain.twins.isTwinCard +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.network.NetworkConnectivity +import com.tangem.tap.preferencesStorage +import com.tangem.tap.scope +import com.tangem.tap.store +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class CheckSignedHashesMiddleware { + fun handle(action: WalletAction.CheckSignedHashes, globalState: GlobalState?) { + when (action) { + is WalletAction.CheckSignedHashes.CheckIfWarningNeeded -> { + val validator = globalState?.scanNoteResponse?.walletManager as? SignatureCountValidator + globalState?.scanNoteResponse?.card?.let { card -> + globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local) + if (card.getType() != CardType.Release) addWarningMessage(WarningMessagesManager.devCardWarning()) + if (!preferencesStorage.wasCardScannedBefore(card.cardId)) { + checkIfWarningNeeded(card, validator)?.let { addWarningMessage(it) } + } + updateWarningMessages() + } + + } + is WalletAction.CheckSignedHashes.CheckHashesCountOnline -> checkHashesCountOnline() + is WalletAction.CheckSignedHashes.SaveCardId -> { + val cardId = globalState?.scanNoteResponse?.card?.cardId + cardId?.let { preferencesStorage.saveScannedCardId(it) } + } + } + } + + private fun checkIfWarningNeeded( + card: Card, signatureCountValidator: SignatureCountValidator? = null, + ): WarningMessage? { + if (card.isTwinCard() || card.isMultiwalletAllowed) return null + + return if (signatureCountValidator == null) { + if (card.walletSignedHashes ?: 0 > 0) { + WarningMessagesManager.alreadySignedHashesWarning() + } else { + store.dispatch(WalletAction.CheckSignedHashes.SaveCardId) + null + } + } else { + store.dispatch(WalletAction.CheckSignedHashes.NeedToCheckHashesCountOnline) + null + } + } + + private fun checkHashesCountOnline() { + if (store.state.walletState.hashesCountVerified != false) return + if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return + + val card = store.state.globalState.scanNoteResponse?.card + if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) return + + if (card.isTwinCard() || card.isMultiwalletAllowed) return + + val validator = store.state.globalState.scanNoteResponse?.walletManager + as? SignatureCountValidator + scope.launch { + val result = validator?.validateSignatureCount(card.walletSignedHashes ?: 0) + withContext(Dispatchers.Main) { + when (result) { + SimpleResult.Success -> { + store.dispatch(WalletAction.CheckSignedHashes.ConfirmHashesCount) + store.dispatch(WalletAction.CheckSignedHashes.SaveCardId) + } + is SimpleResult.Failure -> + if (result.error is BlockchainSdkError.SignatureCountNotMatched) { + addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true) + } else if (card.walletSignedHashes ?: 0 > 0) { + addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true) + } + } + } + } + } + + private fun addWarningMessage(warning: WarningMessage, autoUpdate: Boolean = false) { + store.state.globalState.warningManager?.addWarning(warning) + if (autoUpdate) updateWarningMessages() + } + + private fun updateWarningMessages() { + val warningManager = store.state.globalState.warningManager ?: return + store.dispatch(WalletAction.SetWarnings(warningManager.getWarnings(WarningMessage.Location.MainScreen))) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt new file mode 100644 index 0000000000..bf1cceeed7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -0,0 +1,72 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.TokenManager +import com.tangem.blockchain.extensions.Result +import com.tangem.tap.common.redux.global.GlobalState +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.currenciesRepository +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.scope +import com.tangem.tap.store +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class MultiWalletMiddleware { + fun handle( + action: WalletAction.MultiWallet, walletState: WalletState?, globalState: GlobalState? + ) { + when (action) { + is WalletAction.MultiWallet.SelectWallet -> { + if (action.walletData != null) { + store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletDetails)) + } + } + is WalletAction.MultiWallet.AddToken -> { + globalState?.scanNoteResponse?.card?.cardId?.let { + currenciesRepository.saveAddedToken(it, action.token) + } + addToken(action.token, walletState) + } + is WalletAction.MultiWallet.AddTokens -> { + action.tokens.map { addToken(it, walletState) } + } + is WalletAction.MultiWallet.AddBlockchain -> { + globalState?.scanNoteResponse?.card?.cardId?.let { + currenciesRepository.saveAddedBlockchain(it, action.blockchain) + } + store.dispatch(WalletAction.LoadFiatRate(currency = action.blockchain.currency)) + } + is WalletAction.MultiWallet.RemoveWallet -> { + val cardId = globalState?.scanNoteResponse?.card?.cardId + if (action.walletData.token != null) { + cardId?.let { currenciesRepository.removeToken(it, action.walletData.token) } + } else if (action.walletData.blockchain != null) { + cardId?.let { currenciesRepository.removeBlockchain(it, action.walletData.blockchain) } + } + } + } + } + + private fun addToken(token: Token, walletState: WalletState?) { + val tokenManager = + (walletState?.getWalletManager(Blockchain.Ethereum.currency) as? TokenManager) + scope.launch { + val result = tokenManager?.addToken(token) + withContext(Dispatchers.Main) { + when (result) { + is Result.Success -> { + store.dispatch(WalletAction.LoadFiatRate(currency = token.symbol)) + store.dispatch(WalletAction.MultiWallet.TokenLoaded(result.data)) + } + is Result.Failure -> null + null -> null + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TopUpMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TopUpMiddleware.kt new file mode 100644 index 0000000000..c155519eae --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TopUpMiddleware.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import android.net.Uri +import androidx.browser.customtabs.CustomTabsIntent +import com.tangem.tap.domain.TopUpHelper +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.store + +class TopUpMiddleware { + fun handle(action: WalletAction.TopUpAction) { + when (action) { + is WalletAction.TopUpAction.TopUp -> { + val selectedWalletData = store.state.walletState.getSelectedWalletData() + val config = store.state.globalState.configManager?.config ?: return + val defaultAddress = selectedWalletData?.walletAddresses!!.list[0].address + val url = TopUpHelper.getUrl( + selectedWalletData.currencyData.currencySymbol!!, + defaultAddress, + config.moonPayApiKey, + config.moonPayApiSecretKey + ) + val customTabsIntent = CustomTabsIntent.Builder() + .setToolbarColor(action.toolbarColor) + .build() + customTabsIntent.launchUrl(action.context, Uri.parse(url)); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TwinsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TwinsMiddleware.kt new file mode 100644 index 0000000000..88ba60d1d1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TwinsMiddleware.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.preferencesStorage +import com.tangem.tap.store + +class TwinsMiddleware { + fun handle(action: WalletAction.TwinsAction) { + when (action) { + is WalletAction.TwinsAction.SetTwinCard -> { + val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown() + if (showOnboarding) store.dispatch(WalletAction.TwinsAction.ShowOnboarding) + } + WalletAction.TwinsAction.SetOnboardingShown -> { + preferencesStorage.saveTwinsOnboardingShown() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt new file mode 100644 index 0000000000..a82debea08 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -0,0 +1,230 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import android.content.Intent +import android.net.Uri +import androidx.core.content.ContextCompat +import com.tangem.blockchain.common.* +import com.tangem.common.CompletionResult +import com.tangem.tap.* +import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler +import com.tangem.tap.common.extensions.copyToClipboard +import com.tangem.tap.common.extensions.shareText +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.domain.extensions.toSendableAmounts +import com.tangem.tap.domain.twins.TwinsHelper +import com.tangem.tap.features.details.redux.DetailsAction +import com.tangem.tap.features.details.redux.twins.CreateTwinWallet +import com.tangem.tap.features.send.redux.PrepareSendScreen +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.network.NetworkStateChanged +import kotlinx.coroutines.launch +import org.rekotlin.Action +import org.rekotlin.Middleware + +class WalletMiddleware { + private val topUpMiddleware = TopUpMiddleware() + private val twinsMiddleware = TwinsMiddleware() + private val checkSignedHashesMiddleware = CheckSignedHashesMiddleware() + private val multiWalletMiddleware = MultiWalletMiddleware() + + val walletMiddleware: Middleware = { dispatch, state -> + { next -> + { action -> + val globalState = state()?.globalState + val walletState = state()?.walletState + when (action) { + is WalletAction.TopUpAction -> topUpMiddleware.handle(action) + is WalletAction.TwinsAction -> twinsMiddleware.handle(action) + is WalletAction.CheckSignedHashes -> + checkSignedHashesMiddleware.handle(action, globalState) + is WalletAction.MultiWallet -> + multiWalletMiddleware.handle(action, walletState, globalState) + + is WalletAction.LoadWallet -> { + scope.launch { + if (action.currency == null) { + walletState?.walletManagers?.map { walletManager -> + globalState?.tapWalletManager?.loadWalletData(walletManager) + } + } else { + val walletManager = walletState?.getWalletManager(action.currency) + walletManager?.let { globalState?.tapWalletManager?.loadWalletData(it) } + } + } + } + is WalletAction.LoadFiatRate -> { + scope.launch { + if (action.wallet != null) { + globalState?.tapWalletManager?.loadFiatRate( + globalState.appCurrency, action.wallet + ) + } else if (action.currency != null) { + globalState?.tapWalletManager?.loadFiatRate( + globalState.appCurrency, action.currency + ) + } else { + walletState?.wallets?.filter { it.blockchain != null } + ?.map { walletState.getWalletManager(it.blockchain?.currency) } + ?.mapNotNull { walletManager -> + walletManager?.let { + globalState?.tapWalletManager?.loadFiatRate( + globalState.appCurrency, walletManager.wallet + ) + } + } + } + } + } + is WalletAction.CreateWallet -> { + if (walletState?.twinCardsState != null) { + store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning( + globalState?.scanNoteResponse?.card?.cardId?.let { + TwinsHelper.getTwinCardNumber(it) + }, + CreateTwinWallet.CreateWallet + )) + } else { + scope.launch { + val result = tangemSdkManager.createWallet( + globalState?.scanNoteResponse?.card?.cardId + ) + when (result) { + is CompletionResult.Success -> { + globalState?.tapWalletManager?.onCardScanned(result.data) + } + } + } + } + } + is WalletAction.UpdateWallet -> { + if (action.currency != null) { + scope.launch { + val walletManager = walletState?.getWalletManager(action.currency) + walletManager?.let { globalState?.tapWalletManager?.updateWallet(it) } + } + } else { + scope.launch { + if (walletState?.state == ProgressState.Done) { + walletState.walletManagers.map { walletManager -> + globalState?.tapWalletManager?.updateWallet(walletManager) + } + } + } + } + + } + is WalletAction.LoadWallet.Success -> { + store.dispatch(WalletAction.CheckSignedHashes.CheckHashesCountOnline) + } + is WalletAction.Scan -> { + scope.launch { + val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler) + when (result) { + is CompletionResult.Success -> { + tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card) + globalState?.tapWalletManager + ?.onCardScanned(result.data, true) + if (walletState?.twinCardsState != null) { + val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown() + if (showOnboarding) { + store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding)) + } + } + } + } + } + } + is WalletAction.LoadData -> { + scope.launch { + val scanNoteResponse = globalState?.scanNoteResponse ?: return@launch + if (!walletState?.wallets.isNullOrEmpty()) { + globalState.tapWalletManager.reloadData(scanNoteResponse) + } else { + globalState.tapWalletManager.loadData(scanNoteResponse) + } + } + } + is NetworkStateChanged -> { + globalState?.scanNoteResponse?.let { scanNoteResponse -> + store.dispatch(WalletAction.CheckSignedHashes.CheckHashesCountOnline) + scope.launch { + globalState.tapWalletManager.loadData(scanNoteResponse) + } + } + } + is WalletAction.CopyAddress -> { + action.context.copyToClipboard(action.address) + store.dispatch(WalletAction.CopyAddress.Success) + } + is WalletAction.ShareAddress -> { + action.context.shareText(action.address) + } + is WalletAction.ExploreAddress -> { + val uri = Uri.parse(action.exploreUrl) + val intent = Intent(Intent.ACTION_VIEW, uri) + ContextCompat.startActivity(action.context, intent, null) + } + is WalletAction.Send -> { + val newAction = prepareSendAction(action.amount, state()?.walletState) + store.dispatch(newAction) + if (newAction is PrepareSendScreen) { + store.dispatch(NavigationAction.NavigateTo(AppScreen.Send)) + } + } + } + next(action) + } + } + } + + private fun prepareSendAction(amount: Amount?, state: WalletState?): Action { + val selectedWalletData = state?.getSelectedWalletData() + val currency = selectedWalletData?.currencyData?.currencySymbol + val wallet = currency?.let { state.getWalletManager(currency)?.wallet } + + return if (amount != null) { + if (amount.type is AmountType.Token) { + prepareSendActionForToken(amount, state, selectedWalletData, wallet) + } else { + PrepareSendScreen(amount, selectedWalletData?.fiatRate) + } + } else { + val amounts = wallet?.amounts?.toSendableAmounts() + if (currency != null && state.isMultiwalletAllowed) { + val amountToSend = amounts?.find { it.currencySymbol == currency } + ?: return WalletAction.Send.ChooseCurrency(amounts) + if (amountToSend.type is AmountType.Token) { + prepareSendActionForToken(amount, state, selectedWalletData, wallet) + } else { + PrepareSendScreen(amountToSend, selectedWalletData.fiatRate) + } + } else { + if (amounts?.size ?: 0 > 1) { + WalletAction.Send.ChooseCurrency(amounts) + } else { + val amountToSend = amounts?.first() + PrepareSendScreen(amountToSend, selectedWalletData?.fiatRate) + } + } + } + } + + private fun prepareSendActionForToken( + amount: Amount?, state: WalletState?, selectedWalletData: WalletData?, wallet: Wallet? + ): PrepareSendScreen { + val coinRate = state?.getWalletData(wallet?.blockchain?.currency)?.fiatRate + val tokenRate = if (state?.isMultiwalletAllowed == true) { + selectedWalletData?.fiatRate + } else { + selectedWalletData?.currencyData?.token?.fiatRate + } + return PrepareSendScreen( + wallet?.amounts?.get(AmountType.Coin), coinRate, + amount, tokenRate) + } +} diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt new file mode 100644 index 0000000000..b88adcb40d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt @@ -0,0 +1,160 @@ +package com.tangem.tap.features.wallet.redux.reducers + +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.isZero +import com.tangem.tap.common.extensions.toFiatString +import com.tangem.tap.common.extensions.toFormattedCurrencyString +import com.tangem.tap.domain.getFirstToken +import com.tangem.tap.features.wallet.models.removeUnknownTransactions +import com.tangem.tap.features.wallet.models.toPendingTransactions +import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.ui.BalanceStatus +import com.tangem.tap.features.wallet.ui.BalanceWidgetData +import com.tangem.tap.features.wallet.ui.TokenData +import com.tangem.tap.store + +class MultiWalletReducer { + fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState { + return when (action) { + is WalletAction.MultiWallet.AddWalletManagers -> { + state.copy( + walletManagers = action.walletManagers + ) + } + is WalletAction.MultiWallet.AddBlockchains -> { + val wallets = action.blockchains.map { blockchain -> + val wallet = state.getWalletManager(blockchain.currency)?.wallet + val cardToken = if (!state.isMultiwalletAllowed) { + wallet?.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) } + } else { + null + } + WalletData( + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + blockchain.fullName, + currencySymbol = blockchain.currency, + token = cardToken + ), + walletAddresses = createAddressList(wallet), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = false), + blockchain = blockchain + ) + } + + val selectedWallet = if (!state.isMultiwalletAllowed) { + wallets[0].currencyData.currencySymbol + } else { + state.selectedWallet + } + state.copy( + wallets = wallets, + selectedWallet = selectedWallet + ) + } + is WalletAction.MultiWallet.AddBlockchain -> { + val wallet = state.getWalletManager(action.blockchain.currency)?.wallet + val walletData = WalletData( + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + action.blockchain.fullName, + currencySymbol = action.blockchain.currency, + ), + walletAddresses = createAddressList(wallet), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = false), + blockchain = action.blockchain + ) + val newState = state.copy(wallets = state.replaceWalletInWallets(walletData)) + if (wallet != null && wallet.amounts[AmountType.Coin]?.value != null) { + OnWalletLoadedReducer().reduce(wallet, newState) + } else { + newState + } + } + is WalletAction.MultiWallet.AddTokens -> { + if (!state.isMultiwalletAllowed) return state + val walletAddresses = createAddressList(state.getWalletManager(Blockchain.Ethereum.currency)?.wallet) + val wallets = action.tokens.map { token -> + WalletData( + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + currency = token.name, + currencySymbol = token.symbol + ), + walletAddresses = walletAddresses, + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = false), + token = token + ) + } + state.copy(wallets = state.wallets + wallets) + } + is WalletAction.MultiWallet.AddToken -> { + if (!state.isMultiwalletAllowed) return state + val walletAddresses = createAddressList(state.getWalletManager(Blockchain.Ethereum.currency)?.wallet) + val wallet = WalletData( + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + currency = action.token.name, + currencySymbol = action.token.symbol + ), + walletAddresses = walletAddresses, + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = false), + token = action.token + ) + val wallets = state.replaceWalletInWallets(wallet) + state.copy(wallets = wallets) + } + is WalletAction.MultiWallet.TokenLoaded -> { + val pendingTransactions = state.getWalletManager(Blockchain.Ethereum.currency) + ?.wallet?.let { wallet -> + wallet.recentTransactions.toPendingTransactions(wallet.address) + } ?: emptyList() + + val sendButtonEnabled = action.amount.value?.isZero() == false && pendingTransactions.isEmpty() + val balanceStatus = if (pendingTransactions.isNotEmpty()) { + BalanceStatus.TransactionInProgress + } else { + BalanceStatus.VerifiedOnline + } + val tokenWalletData = state.getWalletData(action.amount.currencySymbol) + val newTokenWalletData = tokenWalletData?.copy( + currencyData = tokenWalletData.currencyData.copy( + status = balanceStatus, + amount = action.amount.value?.toFormattedCurrencyString( + action.amount.decimals, action.amount.currencySymbol + ), + fiatAmount = tokenWalletData.fiatRate?.let { + action.amount.value + ?.toFiatString(it, store.state.globalState.appCurrency) + } + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled) + ) + val wallets = state.replaceWalletInWallets(newTokenWalletData) + state.copy(wallets = wallets) + } + is WalletAction.MultiWallet.SetIsMultiwalletAllowed -> + state.copy(isMultiwalletAllowed = action.isMultiwalletAllowed) + + is WalletAction.MultiWallet.SelectWallet -> + state.copy(selectedWallet = action.walletData?.currencyData?.currencySymbol) + + is WalletAction.MultiWallet.RemoveWallet -> { + state.copy(wallets = state.wallets.filterNot { + it.currencyData.currencySymbol == action.walletData.currencyData.currencySymbol + }) + } + is WalletAction.MultiWallet.SetPrimaryBlockchain -> + state.copy(primaryBlockchain = action.blockchain) + + is WalletAction.MultiWallet.SetPrimaryToken -> + state.copy(primaryToken = action.token) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt new file mode 100644 index 0000000000..59edbdd896 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt @@ -0,0 +1,182 @@ +package com.tangem.tap.features.wallet.redux.reducers + +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Wallet +import com.tangem.common.extensions.isZero +import com.tangem.tap.common.extensions.toFiatString +import com.tangem.tap.common.extensions.toFormattedCurrencyString +import com.tangem.tap.common.extensions.toFormattedString +import com.tangem.tap.domain.getFirstToken +import com.tangem.tap.features.wallet.models.removeUnknownTransactions +import com.tangem.tap.features.wallet.models.toPendingTransactions +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletMainButton +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.BalanceStatus +import com.tangem.tap.features.wallet.ui.BalanceWidgetData +import com.tangem.tap.features.wallet.ui.TokenData +import com.tangem.tap.store +import java.math.RoundingMode + +class OnWalletLoadedReducer { + fun reduce(wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null): WalletState { + if (!walletState.isMultiwalletAllowed) { + return onSingleWalletLoaded(wallet, walletState, topUpAllowed) + } else { + val fiatCurrencySymbol = store.state.globalState.appCurrency + val amount = wallet.amounts[AmountType.Coin]?.value + if (walletState.getWalletData(wallet.blockchain.currency) == null && amount?.isZero() != false) { + return walletState + } + if (wallet.blockchain != Blockchain.Ethereum) { + val formattedAmount = amount?.toFormattedCurrencyString( + wallet.blockchain.decimals(), + wallet.blockchain.currency) + + val pendingTransactions = wallet.recentTransactions + .toPendingTransactions(wallet.address) + + val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty() + val balanceStatus = if (pendingTransactions.isNotEmpty()) { + BalanceStatus.TransactionInProgress + } else { + BalanceStatus.VerifiedOnline + } + val walletData = walletState.getWalletData(wallet.blockchain.currency) + ?: WalletData() + val newWalletData = walletData.copy( + currencyData = walletData.currencyData.copy( + status = balanceStatus, currency = wallet.blockchain.fullName, + currencySymbol = wallet.blockchain.currency, + amount = formattedAmount, + fiatAmount = walletData.fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) } + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled) + ) + val wallets = walletState.replaceWalletInWallets(newWalletData) + val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) { + ProgressState.Loading + } else { + ProgressState.Done + } + return walletState.copy( + state = state, wallets = wallets + ) + } else { + val formattedAmount = amount?.toFormattedCurrencyString( + wallet.blockchain.decimals(), + wallet.blockchain.currency) + + val pendingTransactions = wallet.recentTransactions + .toPendingTransactions(wallet.address) + + val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty() + val balanceStatus = if (pendingTransactions.isNotEmpty()) { + BalanceStatus.TransactionInProgress + } else { + BalanceStatus.VerifiedOnline + } + + val ethereumWalletData = walletState.getWalletData(wallet.blockchain.currency) + ?: WalletData() + val newEthereumWalletData = ethereumWalletData.copy( + currencyData = ethereumWalletData.currencyData.copy( + status = balanceStatus, + amount = formattedAmount, + currency = wallet.blockchain.fullName, + currencySymbol = wallet.blockchain.currency, + fiatAmount = ethereumWalletData.fiatRate?.let { + amount?.toFiatString(it, fiatCurrencySymbol) + } + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled) + ) + + val tokens = wallet.getTokens().mapNotNull { token -> + val tokenWalletData = walletState.getWalletData(token.symbol) + tokenWalletData?.copy( + currencyData = tokenWalletData.currencyData.copy( + status = balanceStatus, + amount = wallet.getTokenAmount(token)?.value?.toFormattedCurrencyString( + token.decimals, token.symbol + ), + fiatAmount = tokenWalletData.fiatRate?.let { + wallet.getTokenAmount(token)?.value + ?.toFiatString(it, fiatCurrencySymbol) + } + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled) + ) + } + val wallets = walletState.replaceSomeWallets((tokens + newEthereumWalletData).filterNotNull()) + val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) { + ProgressState.Loading + } else { + ProgressState.Done + } + return walletState.copy( + state = state, wallets = wallets + ) + } + } + } + + private fun onSingleWalletLoaded(wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null + ): WalletState { + if (wallet.blockchain != walletState.primaryBlockchain) return walletState + + val fiatCurrencySymbol = store.state.globalState.appCurrency + val token = wallet.getFirstToken() + val tokenData = if (token != null) { + val tokenAmount = wallet.getTokenAmount(token) + if (tokenAmount != null) { + val tokenFiatRate = walletState.primaryWallet?.currencyData?.token?.fiatRate + val tokenFiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencySymbol) } + TokenData(tokenAmount.value?.toFormattedString(tokenAmount.decimals) ?: "", + tokenAmount.currencySymbol, tokenFiatAmount) + } else { + null + } + } else { + null + } + val amount = wallet.amounts[AmountType.Coin]?.value + val formattedAmount = amount?.toFormattedCurrencyString( + wallet.blockchain.decimals(), + wallet.blockchain.currency) + val fiatRate = walletState.primaryWallet?.fiatRate + val fiatAmountRaw = fiatRate?.multiply(amount)?.setScale(2, RoundingMode.DOWN) + val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) } + + val pendingTransactions = wallet.recentTransactions + .toPendingTransactions(wallet.address) + + val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty() + val balanceStatus = if (pendingTransactions.isNotEmpty()) { + BalanceStatus.TransactionInProgress + } else { + BalanceStatus.VerifiedOnline + } + val walletData = walletState.primaryWallet?.copy( + currencyData = BalanceWidgetData( + balanceStatus, wallet.blockchain.fullName, + currencySymbol = wallet.blockchain.currency, + formattedAmount, + token = tokenData, + fiatAmount = fiatAmount, + fiatAmountRaw = fiatAmountRaw + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled) + ) + val wallets = walletData?.let { listOf(walletData) } ?: emptyList() + return walletState.copy( + state = ProgressState.Done, wallets = wallets + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TwinsReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TwinsReducer.kt new file mode 100644 index 0000000000..79025ca6cc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TwinsReducer.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.features.wallet.redux.reducers + +import com.tangem.tap.features.wallet.redux.TwinCardsState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletState + +class TwinsReducer { + fun reduce(action: WalletAction.TwinsAction, state: WalletState): WalletState { + return when (action) { + is WalletAction.TwinsAction.SetTwinCard -> { + state.copy( + twinCardsState = TwinCardsState( + secondCardId = action.secondCardId, + cardNumber = action.number, + showTwinOnboarding = state.twinCardsState?.showTwinOnboarding + ?: false, + isCreatingTwinCardsAllowed = action.isCreatingTwinCardsAllowed + ) + ) + } + is WalletAction.TwinsAction.ShowOnboarding -> { + state.copy( + twinCardsState = state.twinCardsState?.copy(showTwinOnboarding = true) + ?: TwinCardsState(null, null, + showTwinOnboarding = true, + isCreatingTwinCardsAllowed = false) + ) + } + is WalletAction.TwinsAction.SetOnboardingShown -> { + state.copy( + twinCardsState = state.twinCardsState?.copy(showTwinOnboarding = false) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index ce7fec6959..ed446c3ee5 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -8,6 +8,7 @@ import com.tangem.wallet.R import kotlinx.android.synthetic.main.card_balance.* import kotlinx.android.synthetic.main.layout_balance.* import kotlinx.android.synthetic.main.layout_balance_error.* +import java.math.BigDecimal enum class BalanceStatus { VerifiedOnline, @@ -25,6 +26,7 @@ data class BalanceWidgetData( val currencySymbol: String? = null, val amount: String? = null, val fiatAmount: String? = null, + val fiatAmountRaw: BigDecimal? = null, val token: TokenData? = null, val amountToCreateAccount: String? = null, val errorMessage: String? = null @@ -33,14 +35,16 @@ data class BalanceWidgetData( data class TokenData( val amount: String, val tokenSymbol: String, - val fiatAmount: String? = null + val fiatAmount: String? = null, + val fiatRateString: String? = null, + val fiatRate: BigDecimal? = null, ) class BalanceWidget( val fragment: Fragment, val data: BalanceWidgetData, - val isTwinCard: Boolean, + private val isTwinCard: Boolean, ) { fun setup() { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt new file mode 100644 index 0000000000..6570cee834 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.features.wallet.ui + +import android.view.View +import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType +import com.tangem.blockchain.blockchains.cardano.CardanoAddressType +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.address.AddressType +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.wallet.R + +class MultipleAddressUiHelper { + companion object { + fun typeToId(type: AddressType): Int { + return when (type) { + is BitcoinAddressType.Legacy -> R.id.chip_legacy + is BitcoinAddressType.Segwit -> R.id.chip_default + is CardanoAddressType.Byron -> R.id.chip_legacy + is CardanoAddressType.Shelley -> R.id.chip_default + else -> View.NO_ID + } + } + + fun idToType(id: Int, currencyName: CryptoCurrencyName?): AddressType? { + val blockchain = currencyName?.let { currency -> + Blockchain.fromCurrency(currency) + } + return idToType(id, blockchain) + } + + fun idToType(id: Int, blockchain: Blockchain?): AddressType? { + return when (id) { + R.id.chip_default -> { + when (blockchain) { + Blockchain.Bitcoin -> BitcoinAddressType.Segwit + Blockchain.CardanoShelley -> CardanoAddressType.Shelley + else -> null + } + } + R.id.chip_legacy -> { + when (blockchain) { + Blockchain.Bitcoin -> BitcoinAddressType.Legacy + Blockchain.CardanoShelley -> CardanoAddressType.Byron + else -> null + } + } + else -> null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt new file mode 100644 index 0000000000..bbdc67d33f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -0,0 +1,289 @@ +package com.tangem.tap.features.wallet.ui + +import android.app.Dialog +import android.os.Bundle +import android.view.* +import androidx.activity.OnBackPressedCallback +import androidx.annotation.IdRes +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.transition.TransitionInflater +import com.tangem.blockchain.common.Blockchain +import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.wallet.models.PendingTransaction +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletDialog +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter +import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.fragment_details_twin_cards.* +import kotlinx.android.synthetic.main.fragment_wallet.* +import kotlinx.android.synthetic.main.fragment_wallet_details.* +import kotlinx.android.synthetic.main.fragment_wallet_details.rv_pending_transaction +import kotlinx.android.synthetic.main.fragment_wallet_details.toolbar +import kotlinx.android.synthetic.main.item_currency_wallet.view.* +import kotlinx.android.synthetic.main.layout_balance_error.* +import kotlinx.android.synthetic.main.layout_balance_wallet_details.* +import kotlinx.android.synthetic.main.layout_wallet_details.* +import org.rekotlin.StoreSubscriber + +class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreSubscriber { + + private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter + private var dialog: Dialog? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + store.dispatch(WalletAction.MultiWallet.SelectWallet(null)) + store.dispatch(NavigationAction.PopBackTo()) + } + }) + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(R.transition.slide_right) + exitTransition = inflater.inflateTransition(R.transition.fade) + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.walletState == newState.walletState + }.select { it.walletState } + } + store.dispatch(WalletAction.UpdateWallet()) + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + toolbar.setNavigationOnClickListener { activity?.onBackPressed() } + + + setupTransactionsRecyclerView() + setupButtons() + } + + + private fun setupTransactionsRecyclerView() { + pendingTransactionAdapter = PendingTransactionsAdapter() + rv_pending_transaction.layoutManager = LinearLayoutManager(requireContext()) + rv_pending_transaction.adapter = pendingTransactionAdapter + } + + private fun setupButtons() { + btn_confirm.text = getString(R.string.wallet_button_send) + btn_confirm.setOnClickListener { store.dispatch(WalletAction.Send()) } + + btn_share.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) } + + btn_top_up.setOnClickListener { + store.dispatch( + WalletAction.TopUpAction.TopUp(requireContext(), R.color.backgroundLightGray) + ) + } + } + + override fun newState(state: WalletState) { + if (activity == null) return + if (state.selectedWallet == null) return + val selectedWallet = state.getSelectedWalletData() ?: return + + tv_currency_title.text = selectedWallet.currencyData.currency + + showPendingTransactionsIfPresent(selectedWallet.pendingTransactions) + setupAddressCard(selectedWallet) + setupBalanceData(selectedWallet.currencyData) + + btn_confirm.isEnabled = selectedWallet.mainButton.enabled + btn_copy.setOnClickListener { + selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> + store.dispatch(WalletAction.CopyAddress(addressString, requireContext())) + } + } + btn_share.setOnClickListener { + selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> + store.dispatch(WalletAction.ShareAddress(addressString, requireContext())) + } + } + + handleDialogs(state.walletDialog) + handleCurrencyIcon(selectedWallet) + + srl_wallet_details.setOnRefreshListener { + if (selectedWallet.currencyData.status != BalanceStatus.Loading) { + store.dispatch(WalletAction.LoadWallet( + selectedWallet.topUpState.allowed, + selectedWallet.currencyData.currencySymbol + )) + } + } + + if (selectedWallet.currencyData.status != BalanceStatus.Loading) { + srl_wallet_details.isRefreshing = false + } + } + + private fun handleCurrencyIcon(wallet: WalletData) { + val blockchain = wallet.currencyData.currencySymbol?.let { Blockchain.fromCurrency(it) } + if (blockchain != null && blockchain != Blockchain.Unknown) { + tv_token_letter.text = null + iv_currency.colorFilter = null + iv_currency.setImageResource(blockchain.getIconRes()) + } else { + tv_token_letter.text = wallet.currencyData.currencySymbol?.take(1) + wallet.token?.getColor()?.let { iv_currency.setColorFilter(it) } + iv_currency.setImageResource(R.drawable.shape_circle) + } + } + + + private fun showPendingTransactionsIfPresent(pendingTransactions: List) { + pendingTransactionAdapter.submitList(pendingTransactions) + rv_pending_transaction?.show(pendingTransactions.isNotEmpty()) + } + + private fun setupAddressCard(state: WalletData) { + if (state.walletAddresses != null) { + if (state.shouldShowMultipleAddress()) { + (card_balance as? ViewGroup)?.beginDelayedTransition() + chip_group_address_type.show() + chip_group_address_type.fitChipsByGroupWidth() + + val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) + if (checkedId != View.NO_ID) chip_group_address_type.check(checkedId) + + chip_group_address_type.setOnCheckedChangeListener { group, checkedId -> + if (checkedId == -1) return@setOnCheckedChangeListener + val type = MultipleAddressUiHelper.idToType(checkedId, state.currencyData.currencySymbol) + type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) } + } + } else { + chip_group_address_type.hide() + } + tv_address.text = state.walletAddresses.selectedAddress.address + tv_explore?.setOnClickListener { + store.dispatch(WalletAction.ExploreAddress( + state.walletAddresses.selectedAddress.exploreUrl, + requireContext())) + } + iv_qr_code.setImageBitmap(state.walletAddresses.selectedAddress.address.toQrCode()) + } + } + + private fun setupBalanceData(data: BalanceWidgetData) { + when (data.status) { + BalanceStatus.Loading -> { + l_balance.show() + l_balance_error.hide() + tv_fiat_amount.hide() + tv_amount.text = "" + + showStatus(R.id.tv_status_loading) + showBalanceWithoutToken(data, false) + } + BalanceStatus.VerifiedOnline, BalanceStatus.TransactionInProgress -> { + l_balance.show() + l_balance_error.hide() + val statusView = if (data.status == BalanceStatus.VerifiedOnline) { + R.id.tv_status_verified + } else { + tv_status_error.text = + getText(R.string.wallet_balance_tx_in_progress) + R.id.group_error + } + showStatus(statusView) + tv_status_error_message.hide() + showBalanceWithoutToken(data, true) + } + BalanceStatus.Unreachable -> { + l_balance.show() + l_balance_error.hide() + tv_fiat_amount.hide() + + tv_amount.text = "" + + tv_status_error_message.text = data.errorMessage + tv_status_error.text = + getString(R.string.wallet_balance_blockchain_unreachable) + + showStatus(R.id.group_error) + tv_status_error_message.show(!data.errorMessage.isNullOrBlank()) + } + + BalanceStatus.NoAccount -> { + l_balance.hide() + l_balance_error.show() + tv_error_title.text = getText(R.string.wallet_error_no_account) + tv_error_descriptions.text = + getString( + R.string.wallet_error_no_account_subtitle_format, + data.amountToCreateAccount, data.currencySymbol + ) + } + } + } + + private fun showStatus(@IdRes viewRes: Int) { + group_error.show(viewRes == R.id.group_error) + tv_status_loading.show(viewRes == R.id.tv_status_loading) + tv_status_verified.show(viewRes == R.id.tv_status_verified) + } + + private fun showBalanceWithoutToken(data: BalanceWidgetData, showAmount: Boolean) { + tv_amount.text = if (showAmount) data.amount else "" + if (showAmount) { + tv_fiat_amount.show() + tv_fiat_amount.text = data.fiatAmount + } + } + + private fun handleDialogs(walletDialog: WalletDialog?) { + when (walletDialog) { + is WalletDialog.SelectAmountToSendDialog -> { + if (dialog == null) dialog = AmountToSendDialog(requireContext()).apply { + this.show(walletDialog.amounts) + } + } + null -> { + dialog?.dismiss() + dialog = null + } + } + } + + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.meny_remove -> { + store.state.walletState.getSelectedWalletData()?.let { walletData -> + store.dispatch(WalletAction.MultiWallet.RemoveWallet(walletData)) + store.dispatch(WalletAction.MultiWallet.SelectWallet(null)) + store.dispatch(NavigationAction.PopBackTo()) + true + } + false + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + if (store.state.walletState.canBeRemoved(store.state.walletState.getSelectedWalletData())) { + inflater.inflate(R.menu.wallet_details, menu) + } + } + +} diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index 749486e3fe..b7582f4972 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -1,9 +1,11 @@ package com.tangem.tap.features.wallet.ui -import android.app.Dialog import android.os.Bundle -import android.view.* +import android.view.Menu import android.view.Menu.NONE +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment @@ -12,39 +14,32 @@ import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionInflater import com.google.android.material.snackbar.Snackbar import com.squareup.picasso.Picasso -import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType -import com.tangem.blockchain.blockchains.cardano.CardanoAddressType -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.address.AddressType import com.tangem.tangem_sdk_new.extensions.dpToPx -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.twins.TwinCardNumber +import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.wallet.redux.* -import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog -import com.tangem.tap.features.wallet.ui.dialogs.PayIdDialog -import com.tangem.tap.features.wallet.ui.dialogs.QrDialog +import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration +import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter +import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView +import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView +import com.tangem.tap.features.wallet.ui.wallet.WalletView import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.card_balance.* import kotlinx.android.synthetic.main.fragment_wallet.* -import kotlinx.android.synthetic.main.layout_address.* -import kotlinx.android.synthetic.main.layout_send_fee.* -import kotlinx.android.synthetic.main.layout_wallet_long_buttons.* -import kotlinx.android.synthetic.main.layout_wallet_short_buttons.* import org.rekotlin.StoreSubscriber class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { - private var dialog: Dialog? = null private var snackbar: Snackbar? = null - private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter private lateinit var warningsAdapter: WarningMessagesAdapter + private var walletView: WalletView = SingleWalletView() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) @@ -65,12 +60,14 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber - tv_twin_card_number.show() - iv_twin_card.show() - val number = when (cardNumber) { - TwinCardNumber.First -> "1" - TwinCardNumber.Second -> "2" - } - tv_twin_card_number.text = getString(R.string.wallet_twins_chip_format, number) - } - if (state.twinCardsState?.cardNumber == null) { - tv_twin_card_number.hide() - iv_twin_card.hide() + if (state.isMultiwalletAllowed && walletView is SingleWalletView) { + walletView = MultiWalletView() + walletView.changeWalletView(this) + } else if (!state.isMultiwalletAllowed && walletView is MultiWalletView) { + walletView = SingleWalletView() + walletView.changeWalletView(this) } + walletView.onNewState(state) - if (!state.showDetails) { + if (!state.shouldShowDetails) { toolbar.menu.removeItem(R.id.details_menu) } else if (toolbar.menu.findItem(R.id.details_menu) == null) { toolbar.menu.add(R.menu.wallet, R.id.details_menu, NONE, R.string.details_title) } - srl_wallet.setOnRefreshListener { - store.dispatch(WalletAction.LoadData) - } - setupNoInternetHandling(state) - - setupButtons(state) - setupAddressCard(state) setupCardImage(state.cardImage) - warningsAdapter.submitList(state.mainWarningsList) - rv_warning_messages.show(state.mainWarningsList.isNotEmpty()) + showWarningsIfPresent(state.mainWarningsList) - pendingTransactionAdapter.submitList(state.pendingTransactions) - rv_pending_transaction.show(state.pendingTransactions.isNotEmpty()) + srl_wallet.setOnRefreshListener { + if (state.state != ProgressState.Loading) { + store.dispatch(WalletAction.LoadData) + } + } - handleDialogs(state.walletDialog) - - l_balance.show() - BalanceWidget(this, state.currencyData, state.twinCardsState != null).setup() - - if (state.state == ProgressState.Done) { + if (state.state != ProgressState.Loading) { srl_wallet.isRefreshing = false } + } - when (state.payIdData.payIdState) { - PayIdState.Disabled, PayIdState.Loading -> group_payid.hide() - PayIdState.NotCreated -> { - group_payid.show() - tv_create_payid.show() - tv_create_payid.setOnClickListener { store.dispatch(WalletAction.CreatePayId) } - tv_payid_address.hide() - } - PayIdState.Created -> { - group_payid.show() - tv_create_payid.hide() - tv_payid_address.show() - tv_payid_address.text = state.payIdData.payId - - } - PayIdState.ErrorLoading -> { - } - } + private fun showWarningsIfPresent(warnings: List) { + warningsAdapter.submitList(warnings) + rv_warning_messages.show(warnings.isNotEmpty()) } private fun setupNoInternetHandling(state: WalletState) { @@ -174,98 +136,14 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber R.string.wallet_button_send - is WalletMainButton.CreateWalletButton -> { - if (state.twinCardsState == null) { - R.string.wallet_button_create_wallet - } else { - R.string.wallet_button_create_twin_wallet - } - } - } - btnConfirm.text = getString(buttonTitle) - btnConfirm.isEnabled = state.mainButton.enabled - - btnConfirm.setOnClickListener { - when (state.mainButton) { - is WalletMainButton.SendButton -> store.dispatch(WalletAction.Send()) - is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet) - } - } - btn_top_up.setOnClickListener { - store.dispatch( - WalletAction.TopUpAction.TopUp(requireContext(), R.color.backgroundLightGray) - ) - } - } - - private fun setupAddressCard(state: WalletState) { - if (state.walletAddresses != null) { - l_address?.show() - if (state.showMultipleAddress) { - (l_address as? ViewGroup)?.beginDelayedTransition() - chip_group_address_type.show() - chip_group_address_type.fitChipsByGroupWidth() - - val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) - if (checkedId != View.NO_ID) chip_group_address_type.check(checkedId) - - chip_group_address_type.setOnCheckedChangeListener { group, checkedId -> - if (checkedId == -1) return@setOnCheckedChangeListener - - MultipleAddressUiHelper.idToType(checkedId, state.wallet?.blockchain)?.let { - store.dispatch(WalletAction.ChangeSelectedAddress(it)) - } - } - } else { - chip_group_address_type.hide() - } - tv_address.text = state.walletAddresses.selectedAddress.address - tv_explore?.setOnClickListener { - store.dispatch(WalletAction.ExploreAddress(requireContext())) - } - } else { - l_address?.hide() - } - } - private fun setupCardImage(cardImage: Artwork?) { Picasso.get() .load(cardImage?.artworkId) @@ -274,47 +152,13 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { - if (walletDialog.qrCode != null && walletDialog.shareUrl != null) { - if (dialog == null) dialog = QrDialog(requireContext()).apply { - this.showQr( - walletDialog.qrCode, walletDialog.shareUrl, walletDialog.currencyName - ) - } - } - } - is WalletDialog.CreatePayIdDialog -> { - when (walletDialog.creatingPayIdState) { - CreatingPayIdState.EnterPayId -> { - if (dialog == null) dialog = PayIdDialog(requireContext()).apply { - this.show() - } - (dialog as? PayIdDialog)?.stopProgress() - } - CreatingPayIdState.Waiting -> (dialog as? PayIdDialog)?.showProgress() - } - } - is WalletDialog.SelectAmountToSendDialog -> { - if (dialog == null) dialog = AmountToSendDialog(requireContext()).apply { - this.show(walletDialog.amounts) - } - } - null -> { - dialog?.dismiss() - dialog = null - } - } - } - override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.details_menu -> { store.state.globalState.scanNoteResponse?.let { scanNoteResponse -> store.dispatch(DetailsAction.PrepareScreen( scanNoteResponse.card, scanNoteResponse, - store.state.walletState.wallet, + store.state.walletState.walletManagers.map { it.wallet }, store.state.globalState.configManager?.config?.isCreatingTwinCardsAllowed, store.state.globalState.appCurrency )) @@ -328,41 +172,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber R.id.chip_legacy - is BitcoinAddressType.Segwit -> R.id.chip_default - is CardanoAddressType.Byron -> R.id.chip_legacy - is CardanoAddressType.Shelley -> R.id.chip_default - else -> View.NO_ID - } - } - - fun idToType(id: Int, blockchain: Blockchain?): AddressType? { - return when (id) { - R.id.chip_default -> { - when (blockchain) { - Blockchain.Bitcoin -> BitcoinAddressType.Segwit - Blockchain.CardanoShelley -> CardanoAddressType.Shelley - else -> null - } - } - R.id.chip_legacy -> { - when (blockchain) { - Blockchain.Bitcoin -> BitcoinAddressType.Legacy - Blockchain.CardanoShelley -> CardanoAddressType.Byron - else -> null - } - } - else -> null - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/PendingTransactionsAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/features/wallet/ui/PendingTransactionsAdapter.kt rename to app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt index 93e0202458..09c6094d38 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/PendingTransactionsAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.wallet.ui +package com.tangem.tap.features.wallet.ui.adapters import android.view.LayoutInflater import android.view.View diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt new file mode 100644 index 0000000000..f8c337d1f1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt @@ -0,0 +1,137 @@ +package com.tangem.tap.features.wallet.ui.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.tap.common.extensions.* +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.ui.BalanceStatus +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.item_currency_wallet.view.* +import java.math.BigDecimal + +class WalletAdapter + : ListAdapter(DiffUtilCallback) { + + override fun getItemId(position: Int): Long { + return currentList[position].currencyData.currencySymbol?.hashCode()?.toLong() ?: 0 + } + + fun submitList(list: List, primaryBlockchain: Blockchain?, primaryToken: Token? = null) { + val listModified = list.toMutableList() + val primaryBlockchainWallet = when ( + val index = listModified.indexOfFirst { it.blockchain == primaryBlockchain } + ) { + -1 -> null + else -> listModified.removeAt(index) + } + + + val primaryTokenWallet = if (primaryToken == null) null else when ( + val index = listModified.indexOfFirst { it.token == primaryToken } + ) { + -1 -> null + else -> listModified.removeAt(index) + } + + if (list.all { it.currencyData.fiatAmount == null }) { + val sortedList = listOfNotNull(primaryBlockchainWallet, primaryTokenWallet) + listModified + super.submitList(sortedList) + return + } + + val sorted = listModified.sortedWith( + compareByDescending { it.currencyData.fiatAmountRaw ?: BigDecimal.ZERO } + .thenBy { it.currencyData.currencySymbol } + ) + val sortedList = listOfNotNull(primaryBlockchainWallet, primaryTokenWallet) + sorted + super.submitList(sortedList) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WalletsViewHolder { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.item_currency_wallet, parent, false) + return WalletsViewHolder(layout) + } + + override fun onBindViewHolder(holder: WalletsViewHolder, position: Int) { + holder.bind(currentList[position]) + } + + object DiffUtilCallback : DiffUtil.ItemCallback() { + override fun areContentsTheSame( + oldItem: WalletData, newItem: WalletData + ) = oldItem == newItem + + override fun areItemsTheSame( + oldItem: WalletData, newItem: WalletData + ) = oldItem == newItem + } + + class WalletsViewHolder(val view: View) : + RecyclerView.ViewHolder(view) { + + fun bind(wallet: WalletData) { + view.tv_currency.text = wallet.currencyData.currency + view.tv_amount.text = wallet.currencyData.amount + if (view.tv_amount.isEllipsized()) { + val allowedSize = view.tv_amount.text.length - 4 + view.tv_amount.text = wallet.currencyData.amount?.ellipsizeBeforeSpace(allowedSize) + } + view.tv_amount_fiat.text = wallet.currencyData.fiatAmount + view.tv_exchange_rate.text = wallet.fiatRateString + view.card_wallet.setOnClickListener { + store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet)) + } + val blockchain = wallet.currencyData.currencySymbol?.let { Blockchain.fromCurrency(it) } + if (blockchain != null && blockchain != Blockchain.Unknown) { + view.tv_token_letter.text = null + view.iv_currency.colorFilter = null + view.iv_currency.setImageResource(blockchain.getIconRes()) + } else { + view.tv_token_letter.text = wallet.currencyData.currencySymbol?.take(1) + wallet.token?.getColor()?.let { view.iv_currency.setColorFilter(it) } + view.iv_currency.setImageResource(R.drawable.shape_circle) + } + when (wallet.currencyData.status) { + BalanceStatus.VerifiedOnline -> hideWarning() + BalanceStatus.Loading -> { + hideWarning() + if (wallet.currencyData.amount == null) { + view.tv_exchange_rate.text = view.context.getText(R.string.wallet_balance_loading) + } + } + BalanceStatus.TransactionInProgress -> + showWarning(view.context.getString(R.string.wallet_balance_tx_in_progress)) + BalanceStatus.Unreachable -> + showWarning(view.context.getString(R.string.wallet_balance_blockchain_unreachable)) + + BalanceStatus.NoAccount -> + showWarning(view.context.getString(R.string.wallet_error_no_account)) + else -> { + } + } + } + + private fun showWarning(message: String) { + toggleWarning(true) + view.tv_status_error_message.text = message + } + + private fun hideWarning() { + toggleWarning(false) + } + + private fun toggleWarning(show: Boolean) { + view.tv_exchange_rate.show(!show) + view.tv_status_error_message.show(show) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WarningMessagesAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/features/wallet/ui/WarningMessagesAdapter.kt rename to app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt index e7a2a559e3..3200ac34ec 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WarningMessagesAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.wallet.ui +package com.tangem.tap.features.wallet.ui.adapters import android.graphics.Rect import android.view.LayoutInflater diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/PayIdDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/PayIdDialog.kt deleted file mode 100644 index dd1944ac74..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/PayIdDialog.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tangem.tap.features.wallet.ui.dialogs - -import android.app.Dialog -import android.content.Context -import android.view.View -import com.tangem.tap.common.extensions.hide -import com.tangem.tap.common.extensions.show -import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.notificationsHandler -import com.tangem.tap.store -import com.tangem.wallet.R -import kotlinx.android.synthetic.main.dialog_create_payid.* - -class PayIdDialog(context: Context) : Dialog(context) { - - init { - this.setContentView(R.layout.dialog_create_payid) - } - - override fun show() { - notificationsHandler?.replaceBaseLayout(this.cl_payid_dialog) - this.setOnDismissListener { - notificationsHandler?.returnBaseLayout() - store.dispatch(WalletAction.CreatePayId.Cancel) - } - this.btn_create_payid?.setOnClickListener { - if (this.et_payid.text.isNullOrBlank()) { - store.dispatch(WalletAction.CreatePayId.EmptyField) - } else { - val payid = this.et_payid.text!!.toString() + - this.context.getString(R.string.wallet_create_payid_domain) - store.dispatch(WalletAction.CreatePayId.CompleteCreatingPayId(payid)) - } - } - super.show() - } - - fun showProgress() { - this.btn_create_payid?.visibility = View.INVISIBLE - this.pb_create_payid?.show() - } - - fun stopProgress() { - this.btn_create_payid?.show() - this.pb_create_payid?.hide() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt new file mode 100644 index 0000000000..2fb7ab8876 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -0,0 +1,126 @@ +package com.tangem.tap.features.wallet.ui.wallet + +import androidx.recyclerview.widget.LinearLayoutManager +import com.tangem.tap.common.extensions.hide +import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.tokens.redux.TokensAction +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.BalanceStatus +import com.tangem.tap.features.wallet.ui.WalletFragment +import com.tangem.tap.features.wallet.ui.adapters.WalletAdapter +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.card_balance.* +import kotlinx.android.synthetic.main.fragment_wallet.* +import kotlinx.android.synthetic.main.layout_balance_error.* +import kotlinx.android.synthetic.main.layout_wallet_long_buttons.* + + +class MultiWalletView : WalletView { + + private var fragment: WalletFragment? = null + private lateinit var walletsAdapter: WalletAdapter + + override fun changeWalletView(fragment: WalletFragment) { + setFragment(fragment) + onViewCreated() + showMultiWalletView() + setupButtons() + } + + + private fun showMultiWalletView() { + val fragment = fragment ?: return + fragment.tv_twin_card_number.hide() + fragment.iv_twin_card.hide() + fragment.rv_pending_transaction.hide() + fragment.l_card_balance.hide() + fragment.l_address.hide() + fragment.l_buttons_short.hide() + fragment.l_buttons_long.hide() + fragment.btn_scan_multiwallet?.show() + fragment.rv_multiwallet.show() + fragment.btn_add_token.show() + } + + private fun setupButtons() { + val fragment = fragment ?: return + fragment.btn_scan_multiwallet?.setOnClickListener { store.dispatch(WalletAction.Scan) } + } + + override fun setFragment(fragment: WalletFragment) { + this.fragment = fragment + } + + override fun removeFragment() { + this.fragment = null + } + + override fun onViewCreated() { + setupWalletsRecyclerView() + } + + private fun setupWalletsRecyclerView() { + val fragment = fragment ?: return + walletsAdapter = WalletAdapter() + walletsAdapter.setHasStableIds(true) + fragment.rv_multiwallet.layoutManager = LinearLayoutManager(fragment.requireContext()) + fragment.rv_multiwallet.adapter = walletsAdapter + } + + override fun onNewState(state: WalletState) { + val fragment = fragment ?: return + + walletsAdapter.submitList(state.wallets, state.primaryBlockchain, state.primaryToken) + + fragment.btn_add_token?.setOnClickListener { + store.dispatch(TokensAction.LoadCurrencies) + store.dispatch(TokensAction.SetAddedCurrencies(state.wallets)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens)) + } + handleErrorStates(state, fragment) + } + + private fun handleErrorStates(state: WalletState, fragment: WalletFragment) { + when (state.primaryWallet?.currencyData?.status) { + BalanceStatus.EmptyCard -> { + showErrorState( + fragment, + fragment.getText(R.string.wallet_error_empty_card), + fragment.getString(R.string.wallet_error_empty_card_subtitle) + ) + configureButtonsForEmptyWalletState(fragment) + } + BalanceStatus.UnknownBlockchain -> { + showErrorState( + fragment, + fragment.getText(R.string.wallet_error_unsupported_blockchain), + fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle) + ) + } + } + } + + private fun showErrorState( + fragment: WalletFragment, errorTitle: CharSequence, errorDescription: CharSequence + ) { + fragment.l_card_balance.show() + fragment.l_balance.hide() + fragment.l_balance_error.show() + fragment.rv_multiwallet.hide() + fragment.btn_add_token.hide() + fragment.tv_error_title.text = errorTitle + fragment.tv_error_descriptions.text = errorDescription + } + + private fun configureButtonsForEmptyWalletState(fragment: WalletFragment) { + fragment.btn_scan_multiwallet.hide() + fragment.l_buttons_long.show() + fragment.btn_scan_long.setOnClickListener { store.dispatch(WalletAction.Scan) } + fragment.btn_confirm_long.setOnClickListener { store.dispatch(WalletAction.CreateWallet) } + fragment.btn_confirm_long.text = fragment.getText(R.string.wallet_button_create_wallet) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt new file mode 100644 index 0000000000..34cab5caf0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -0,0 +1,236 @@ +package com.tangem.tap.features.wallet.ui.wallet + +import android.app.Dialog +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.recyclerview.widget.LinearLayoutManager +import com.tangem.tap.common.extensions.* +import com.tangem.tap.domain.twins.TwinCardNumber +import com.tangem.tap.features.wallet.models.PendingTransaction +import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.ui.BalanceWidget +import com.tangem.tap.features.wallet.ui.MultipleAddressUiHelper +import com.tangem.tap.features.wallet.ui.WalletFragment +import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter +import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog +import com.tangem.tap.features.wallet.ui.dialogs.QrDialog +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.card_balance.* +import kotlinx.android.synthetic.main.fragment_wallet.* +import kotlinx.android.synthetic.main.layout_address.* +import kotlinx.android.synthetic.main.layout_wallet_long_buttons.* +import kotlinx.android.synthetic.main.layout_wallet_short_buttons.* + +class SingleWalletView : WalletView { + + private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter + private var fragment: WalletFragment? = null + private var dialog: Dialog? = null + + override fun setFragment(fragment: WalletFragment) { + this.fragment = fragment + } + + override fun removeFragment() { + fragment = null + } + + override fun changeWalletView(fragment: WalletFragment) { + setFragment(fragment) + onViewCreated() + showSingleWalletView() + } + + private fun showSingleWalletView() { + val fragment = fragment ?: return + fragment.rv_multiwallet.hide() + fragment.btn_add_token.hide() + fragment.btn_scan_multiwallet?.hide() + fragment.rv_pending_transaction.hide() + fragment.l_card_balance.show() + fragment.l_address.show() + } + + override fun onViewCreated() { + setupTransactionsRecyclerView() + } + + + private fun setupTransactionsRecyclerView() { + val fragment = fragment ?: return + pendingTransactionAdapter = PendingTransactionsAdapter() + fragment.rv_pending_transaction.layoutManager = LinearLayoutManager(fragment.requireContext()) + fragment.rv_pending_transaction.adapter = pendingTransactionAdapter + } + + override fun onNewState(state: WalletState) { + setupTwinCards(state.twinCardsState) + state.primaryWallet ?: return + setupButtons(state.primaryWallet, state.twinCardsState != null) + setupAddressCard(state.primaryWallet) + showPendingTransactionsIfPresent(state.primaryWallet.pendingTransactions) + setupBalance(state, state.primaryWallet) + handleDialogs(state.walletDialog) + } + + private fun showPendingTransactionsIfPresent(pendingTransactions: List) { + pendingTransactionAdapter.submitList(pendingTransactions) + fragment?.rv_pending_transaction?.show(pendingTransactions.isNotEmpty()) + } + + + private fun setupBalance(state: WalletState, primaryWallet: WalletData) { + fragment?.apply { + this.l_balance.show() + BalanceWidget(this, primaryWallet.currencyData, state.twinCardsState != null).setup() + } + } + + private fun setupTwinCards(twinCardsState: TwinCardsState?) { + fragment?.apply { + twinCardsState?.cardNumber?.let { cardNumber -> + this.tv_twin_card_number.show() + this.iv_twin_card.show() + val number = when (cardNumber) { + TwinCardNumber.First -> "1" + TwinCardNumber.Second -> "2" + } + this.tv_twin_card_number.text = + this.getString(R.string.wallet_twins_chip_format, number) + } + if (twinCardsState?.cardNumber == null) { + this.tv_twin_card_number.hide() + this.iv_twin_card.hide() + } + } + + } + + private fun setupButtons(state: WalletData, isTwinsWallet: Boolean) { + val fragment = fragment ?: return + + setupButtonsType(state, fragment) + + val btnConfirm = if (state.topUpState.allowed) { + fragment.btn_confirm_short + } else { + fragment.btn_confirm_long + } + val btnScan = if (state.topUpState.allowed) { + fragment.btn_scan_short + } else { + fragment.btn_scan_long + } + + setupConfirmButton(state, btnConfirm, fragment, isTwinsWallet) + + btnScan.setOnClickListener { + store.dispatch(WalletAction.Scan) + } + + fragment.btn_copy.setOnClickListener { + state.walletAddresses?.selectedAddress?.address?.let { addressString -> + store.dispatch(WalletAction.CopyAddress(addressString, fragment.requireContext())) + } + } + fragment.btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) } + + fragment.btn_top_up.setOnClickListener { + store.dispatch( + WalletAction.TopUpAction.TopUp(fragment.requireContext(), R.color.backgroundLightGray) + ) + } + } + + private fun setupButtonsType(state: WalletData, fragment: WalletFragment) { + if (state.topUpState.allowed) { + fragment.l_buttons_long.hide() + fragment.l_buttons_short.show() + } else { + fragment.l_buttons_long.show() + fragment.l_buttons_short.hide() + } + } + + private fun setupConfirmButton( + state: WalletData, btnConfirm: Button, fragment: WalletFragment, isTwinsWallet: Boolean + ) { + val buttonTitle = when (state.mainButton) { + is WalletMainButton.SendButton -> R.string.wallet_button_send + is WalletMainButton.CreateWalletButton -> { + if (!isTwinsWallet) { + R.string.wallet_button_create_wallet + } else { + R.string.wallet_button_create_twin_wallet + } + } + } + btnConfirm.text = fragment.getString(buttonTitle) + btnConfirm.isEnabled = state.mainButton.enabled + btnConfirm.setOnClickListener { + when (state.mainButton) { + is WalletMainButton.SendButton -> store.dispatch(WalletAction.Send()) + is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet) + } + } + } + + + private fun setupAddressCard(state: WalletData) { + val fragment = fragment ?: return + if (state.walletAddresses != null) { + fragment.l_address?.show() + if (state.shouldShowMultipleAddress()) { + (fragment.l_address as? ViewGroup)?.beginDelayedTransition() + fragment.chip_group_address_type.show() + fragment.chip_group_address_type.fitChipsByGroupWidth() + + val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) + if (checkedId != View.NO_ID) fragment.chip_group_address_type.check(checkedId) + + fragment.chip_group_address_type.setOnCheckedChangeListener { group, checkedId -> + if (checkedId == -1) return@setOnCheckedChangeListener + val type = MultipleAddressUiHelper.idToType(checkedId, state.currencyData.currencySymbol) + type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) } + } + } else { + fragment.chip_group_address_type.hide() + } + fragment.tv_address.text = state.walletAddresses.selectedAddress.address + fragment.tv_explore?.setOnClickListener { + store.dispatch(WalletAction.ExploreAddress( + state.walletAddresses.selectedAddress.exploreUrl, + fragment.requireContext())) + } + } else { + fragment.l_address?.hide() + } + } + + private fun handleDialogs(walletDialog: WalletDialog?) { + val fragment = fragment ?: return + when (walletDialog) { + is WalletDialog.QrDialog -> { + if (walletDialog.qrCode != null && walletDialog.shareUrl != null) { + if (dialog == null) dialog = QrDialog(fragment.requireContext()).apply { + this.showQr( + walletDialog.qrCode, walletDialog.shareUrl, walletDialog.currencyName + ) + } + } + } + is WalletDialog.SelectAmountToSendDialog -> { + if (dialog == null) dialog = AmountToSendDialog(fragment.requireContext()).apply { + this.show(walletDialog.amounts) + } + } + null -> { + dialog?.dismiss() + dialog = null + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt new file mode 100644 index 0000000000..ab49865c02 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.features.wallet.ui.wallet + +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.WalletFragment + +interface WalletView { + + fun setFragment(fragment: WalletFragment) + + fun removeFragment() + + fun changeWalletView(fragment: WalletFragment) + + fun onViewCreated() + + fun onNewState(state: WalletState) + +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_binance.png b/app/src/main/res/drawable-hdpi/ic_binance.png new file mode 100644 index 0000000000000000000000000000000000000000..edce12fa1436743b10b832ceb1b477bc773c0401 GIT binary patch literal 3696 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 O0000ipwy=z$|Pty}tYIJ?ED{&fVVY9zhdN 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_btc_cash.png b/app/src/main/res/drawable-hdpi/ic_btc_cash.png new file mode 100644 index 0000000000000000000000000000000000000000..ec0864fdb8c1fcbf5f535021e920c3200c833718 GIT binary patch literal 4067 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 V2mk(#rp^EW002ovPDHLkV1l9TsM`Pl literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_cardano.png b/app/src/main/res/drawable-hdpi/ic_cardano.png new file mode 100644 index 0000000000000000000000000000000000000000..8a4ac01ad4ba5e43e9412bb81420316bd57fc092 GIT binary patch literal 4813 zcmV;;5;EYmB)W;?{n_(#!N3{Gy_AXCP2UpKoUSP2r>xVP>q7xK_IATQB)(e-Otx0mUP*+ zf&xJTLRVXwhkz}OG-^^oLI|S}Nf;6c5JEx%$s6yz=bXJ)e>nHPyfF#1cCRn5-mSCt zUjMzMmM&{#nHlh;*8Kp|Shy;o{paa!CB5VWM zh&N9P_Nlcij@wpUr%+JaIuoTu{6~>ZJ~W4_6qdG{*4CvC;FFg>^RX!4Jk)Q|pT#;aVd>MsZ~wT*P(jxxEuRK03(A+Ed!MA@tD%D8!;z{Y z9z~5Yfz8z7;$l(N-=hit{Kq>^c(=J(O-l<32fww03((wbXld~PCtvy0B{m3evO#`q zQf^0;L`3aJ12Pn;s)`dML6E7(C&iuKCD%Q6+sE(iFV9ePKw%1nRkpQtsXPAi|E#GE z#{bBMxy!sOw!pvUPz^N-vd4(I+;nT(GMuZ=Zepu}NW5t1NDQBj?rJ zAA~hS0z$xfrDfSnOD9o| z2dK{%2UMYeX>DEV{Lc4&66P8fB;~evP(V=i zr3|kR8CDmd47_&lm3-E4_rp86_Li5qZ_RFM^9I-dnksM28X>d?wml*eBvE@DL@U6_?T@U| zGw5xmd~u>E5L!8?lTalIvvonayzg`CZa?woz28^$X-aeRx8;v}FK9~Q@=Idu2z7}f z{nc}5iW8+KXXq+=GGSkdOfMw9*DWcKBuYcArM=4$26E^YQt@H~!TUWnjE`PD{E^+X z0JL}mP$;yTmX;QuL|rXGwtl2{u{%^iMp9$(`6Y*O*A26{;FM96B9EyYwJLO%()h2t zj&)D~{mCnY)^OH|Bl*~vJZH=wk)Ae+2qa$lyR*h}*9|lI+fR-rN)#jM zEg2)6K6eD;M&Qm zK~)n{azX8w9gk(0qnhEt&qAmlQp&!Kq_`K)2;xEwFP)y?3?ttw(s)y}}jGghxS@f`!8o~2=~q0W2n`mqQh2n!Wh9Ap(bak%LmkCEwNMDx)Qoej`#LOeCv)kxcUdL z(6VwHA9TcYmK<$e39(aZvX=VVfPB_sM0$*hF+#EIxZ>Qg+rVUg{L%e{PYIi-c{oMwY%x)c4Pt}@l*~AYI3#**cTegr?ZtOEB zjbzu}h&9jdBNIv`bW(B|p%f{ZA>&3>kwCn6?cSTCjdR&~Hq17tb3+;kfG`j`x>H|b zjL=o`oIJaco31*XZ(cfuWoM6NZ+pVr8TA}qxlrv*WmW=_GEvVk#%iZlJ5=F-FLQLgGk44ka8S>YcY?wn2

RK19y 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 z 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_xrp.png b/app/src/main/res/drawable-hdpi/ic_xrp.png new file mode 100644 index 0000000000000000000000000000000000000000..058650f44bc45a7619d45c60e41c77497b3fb64d GIT binary patch literal 1546 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-ldpi/ic_cardano.png b/app/src/main/res/drawable-ldpi/ic_cardano.png new file mode 100644 index 0000000000000000000000000000000000000000..dd8332ef62188998ccca71281c1b89c2d50e3952 GIT binary patch literal 2006 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 P00000NkvXXu0mjfkR32! literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-ldpi/ic_ltc.png b/app/src/main/res/drawable-ldpi/ic_ltc.png new file mode 100644 index 0000000000000000000000000000000000000000..a9dc421e2501bec0e8e035d1f287e3f1e70a2385 GIT binary patch literal 1530 zcmV|*X&p&4pfT0N+@gs>gpjJEL>h(JVz?ZijTGkBytKNroX13a}g&-+dX2@G2ddi6$HFLii zIrObJ-k(1eGMmuaEyK$(x!+sdpjw)5W?1xCjDP`xsG{mX6bt?$@Qj7{$nmGQovGDo zu3oQ)*~ElBdxW8(IC$5At%iFy zpWJ?cJ$sa)A?uuOQcP;KBU-QD83s2WcvMT(-9Bp)WAGy4CgKF}AtEY>2pE_ds!}MM z1yz^0w1>Ric= zW!I+L2r-g4$C2m$M*ZlE^!HT=F&65gzEtTeWv$DPojAN>=LCP!QTA)^I%MHv0Zn_Rrqpp@uzp-zP{`k0h@`jq$} zi0S&R2bVznX2=>Qg6mq53RxalvuX(7`Log%$ik83JXo8<$O9?_d_k3#A zQf~g>b(6#|jE?cz*aiA~%Y-i9si#sRnL4@*K}1A(F41!N*}U2Mk*b?x7K>Yvt5f5X zD{kmi8*%Drw>@9H{bqW5QerI35GhJ9VD#1haA~~N5j0bE5`F7(Z|Z(20H51*V85ij zcW13DUPKb;Tq%u=eLwsPx2#-3jK#gBv$lAF%@5Xj>Gku4%^~_yWlj>Z@qNdiy8F8d zCYH)iLe{(@nkZ%_BD6El@*C!|X7y53g(T7cDKz%xyPO`oKqXCxF`8(iA#aRpst