diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 4b801de36a..82e2081d78 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -37,6 +37,10 @@ import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tokens.UserTokensRepository +import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator +import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation +import com.tangem.tap.domain.walletStores.WalletStoresManager +import com.tangem.tap.domain.walletStores.di.provideDummyImplementation import com.tangem.tap.domain.walletconnect.WalletConnectRepository import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.persistence.CardBalanceStateAdapter @@ -60,6 +64,14 @@ lateinit var shopService: TangemShopService lateinit var assetReader: AssetReader lateinit var userTokensRepository: UserTokensRepository +val walletStoresManager by lazy { + WalletStoresManager.provideDummyImplementation() +} + +val totalFiatBalanceCalculator by lazy { + TotalFiatBalanceCalculator.provideDefaultImplementation() +} + @HiltAndroidApp class TapApplication : Application(), ImageLoaderFactory { 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 6d2bf6af62..f03dfc70ca 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 @@ -16,6 +16,7 @@ import com.tangem.tap.features.shop.redux.ShopReducer import com.tangem.tap.features.tokens.redux.TokensReducer import com.tangem.tap.features.wallet.redux.reducers.WalletReducer import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer import org.rekotlin.Action @@ -40,6 +41,7 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) shopState = ShopReducer.reduce(action, state.shopState), welcomeState = WelcomeReducer.reduce(action, state), saveWalletState = SaveWalletReducer.reduce(action, state), + walletSelectorState = WalletSelectorReducer.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 d85b69df46..c89e7c0133 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 @@ -35,6 +35,7 @@ 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 com.tangem.tap.features.walletSelector.redux.WalletSelectorMiddleware import com.tangem.tap.features.walletSelector.redux.WalletSelectorState import com.tangem.tap.features.welcome.redux.WelcomeMiddleware import com.tangem.tap.features.welcome.redux.WelcomeState @@ -88,6 +89,7 @@ data class AppState( ShopMiddleware().shopMiddleware, WelcomeMiddleware().middleware, SaveWalletMiddleware().middleware, + WalletSelectorMiddleware().middleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt b/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt new file mode 100644 index 0000000000..6bdca4b2de --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt @@ -0,0 +1,25 @@ +package com.tangem.tap.domain.model + +import java.math.BigDecimal + +/** + * Represents fiat balance of [WalletStoreModel] list + * @property amount Amount of the total balance + * */ +sealed class TotalFiatBalance { + open val amount: BigDecimal = BigDecimal.ZERO + + object Loading : TotalFiatBalance() + + class Refreshing( + override val amount: BigDecimal, + ) : TotalFiatBalance() + + class Error( + override val amount: BigDecimal, + ) : TotalFiatBalance() + + class Loaded( + override val amount: BigDecimal, + ) : TotalFiatBalance() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt index 6ce60fae85..4cb6fbfdd8 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt @@ -2,9 +2,17 @@ package com.tangem.tap.domain.model import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.util.UserWalletId -import com.tangem.domain.common.util.userWalletId -import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl +/** + * Represents user's wallet which stored in app persistence + * @param name User's wallet name + * @param walletId User's wallet [UserWalletId] + * @param artworkUrl User wallet card artwork URL + * @param cardsInWallet List of cards IDs assigned with this user's wallet + * @param scanResponse [ScanResponse] of primary user's wallet card. + * TODO: Replace with [com.tangem.domain.common.CardDTO] + * @property cardId ID of user's wallet primary card + * */ data class UserWallet( val name: String, val walletId: UserWalletId, @@ -14,24 +22,4 @@ data class UserWallet( ) { val cardId: String get() = scanResponse.card.cardId - - val hasAccessCode: Boolean - get() = scanResponse.card.isAccessCodeSet - - companion object { - suspend operator fun invoke( - scanResponse: ScanResponse, - backupCardsIds: Set? = null, - ): UserWallet { - return with(scanResponse) { - UserWallet( - walletId = card.userWalletId, - name = productType.name, - artworkUrl = card.getOrLoadCardArtworkUrl(), - cardsInWallet = backupCardsIds?.plus(card.cardId) ?: setOf(card.cardId), - scanResponse = this, - ) - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt b/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt new file mode 100644 index 0000000000..173ed53786 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt @@ -0,0 +1,79 @@ +package com.tangem.tap.domain.model + +import com.tangem.tap.domain.model.WalletDataModel.Status +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.features.wallet.models.PendingTransaction +import com.tangem.tap.features.wallet.redux.AddressData +import java.math.BigDecimal + +/** + * Contains info about wallet store currency + * @param currency Wallet's [Currency] + * @param status Wallet's [Status], represents current status of that currency + * @param walletAddresses List of wallet data [AddressData] + * @param existentialDeposit Amount that must be held on currency's balance, if balance is below that amount all + * founds will be destroyed. Null if currency don't have existential deposit + * @param fiatRate Wallet's fiat rate, used to calculate fiat balance. Null if not provided + * */ +data class WalletDataModel( + val currency: Currency, + val status: Status, + val walletAddresses: List, + val existentialDeposit: BigDecimal?, + val fiatRate: BigDecimal?, +) { + + /** + * Represent current status of currency + * @property amount Currency amount + * @property pendingTransactions List of currency [PendingTransaction] sent in currency's blockchain + * @property errorMessage Status error message, null if not provided + * @property isErrorStatus true if current status is error status, false otherwise + * */ + sealed class Status { + open val amount: BigDecimal = BigDecimal.ZERO + open val pendingTransactions: List = emptyList() + open val errorMessage: String? = null + open val isErrorStatus: Boolean = false + } + + object Loading : Status() + + data class VerifiedOnline( + override val amount: BigDecimal, + ) : Status() + + data class TransactionInProgress( + override val amount: BigDecimal, + override val pendingTransactions: List, + ) : Status() + + data class SameCurrencyTransactionInProgress( + override val amount: BigDecimal, + override val pendingTransactions: List, + ) : Status() + + data class NoAccount( + val amountToCreateAccount: BigDecimal?, + ) : Status() { + override val isErrorStatus: Boolean = true + } + + data class Unreachable( + override val errorMessage: String?, + ) : Status() { + override val isErrorStatus: Boolean = true + } + + object MissedDerivation : Status() { + override val isErrorStatus: Boolean = true + } + + data class Refreshing( + override val amount: BigDecimal, + override val pendingTransactions: List, + override val errorMessage: String?, + ) : Status() { + override val isErrorStatus: Boolean = errorMessage != null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt b/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt new file mode 100644 index 0000000000..1753b9b0ba --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.domain.model + +import com.tangem.blockchain.common.WalletManager +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.domain.model.WalletStoreModel.WalletRent +import com.tangem.tap.domain.tokens.models.BlockchainNetwork +import java.math.BigDecimal + +/** + * Contains info about the blockchain and its currencies + * @param userWalletId ID of the [UserWallet] which uses that store + * @param blockchainNetwork Store's [BlockchainNetwork] + * @param walletManager Store's [WalletManager], may be null if it fails to create this manager + * @param walletsData List of [WalletDataModel] which represents store's blockchain currency and tokens currencies + * @param walletRent Store's [WalletRent], null if store has no rent or currency balance is greater then + * [WalletRent.exemptionAmount] + * */ +data class WalletStoreModel( + val userWalletId: UserWalletId, + val blockchainNetwork: BlockchainNetwork, + val walletManager: WalletManager?, + val walletsData: List, + val walletRent: WalletRent?, +) { + + /** + * Represents wallet blockchain rent + * @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than + * the [WalletRent.exemptionAmount] + * @param exemptionAmount Amount that should be on the blockchain balance not to pay rent + * */ + data class WalletRent( + val rent: BigDecimal, + val exemptionAmount: BigDecimal, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt new file mode 100644 index 0000000000..4ea9db1491 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.domain.model.builders + +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.util.userWalletId +import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl +import com.tangem.tap.domain.model.UserWallet + +class UserWalletBuilder( + private val scanResponse: ScanResponse, +) { + private var backupCardsIds: Set = emptySet() + + fun setBackupCardsIds(backupCardsIds: Set?) = this.apply { + if (backupCardsIds != null) { + this.backupCardsIds = backupCardsIds + } + } + + suspend fun build(): UserWallet { + return with(scanResponse) { + UserWallet( + walletId = card.userWalletId, + name = productType.name, + artworkUrl = card.getOrLoadCardArtworkUrl(), + cardsInWallet = backupCardsIds.plus(card.cardId), + scanResponse = this, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt new file mode 100644 index 0000000000..d9b586d3e6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.domain.totalBalance + +import com.tangem.tap.domain.model.TotalFiatBalance +import com.tangem.tap.domain.model.WalletStoreModel +import java.math.BigDecimal + +interface TotalFiatBalanceCalculator { + suspend fun calculate( + prevAmount: BigDecimal, + walletStores: List, + ): TotalFiatBalance + + companion object +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/di/TotalFiatBalanceCalculatorProvider.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/di/TotalFiatBalanceCalculatorProvider.kt new file mode 100644 index 0000000000..ea285cc404 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/di/TotalFiatBalanceCalculatorProvider.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.domain.totalBalance.di + +import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator +import com.tangem.tap.domain.totalBalance.implementation.DefaultTotalFiatBalanceCalculator + +fun TotalFiatBalanceCalculator.Companion.provideDefaultImplementation(): TotalFiatBalanceCalculator { + return DefaultTotalFiatBalanceCalculator() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt new file mode 100644 index 0000000000..be318667dd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt @@ -0,0 +1,102 @@ +package com.tangem.tap.domain.totalBalance.implementation + +import com.tangem.tap.common.extensions.toFiatValue +import com.tangem.tap.domain.model.TotalFiatBalance +import com.tangem.tap.domain.model.WalletDataModel +import com.tangem.tap.domain.model.WalletStoreModel +import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator { + override suspend fun calculate( + prevAmount: BigDecimal, + walletStores: List, + ): TotalFiatBalance { + return if (walletStores.isEmpty()) { + TotalFiatBalance.Loading + } else { + withContext(Dispatchers.Default) { + val walletsData = walletStores + .asSequence() + .flatMap { it.walletsData } + val calculateAmount = { walletsData.calculateTotalFiatAmount() } + + when (walletsData.findStatus()) { + TotalFiatBalanceStatus.Loading -> TotalFiatBalance.Loading + TotalFiatBalanceStatus.Refreshing -> TotalFiatBalance.Refreshing(prevAmount) + TotalFiatBalanceStatus.Error -> TotalFiatBalance.Error(calculateAmount()) + TotalFiatBalanceStatus.Loaded -> TotalFiatBalance.Loaded(calculateAmount()) + } + } + } + } + + private fun Sequence.findStatus(): TotalFiatBalanceStatus { + return this + .mapToStatus() + .reduce { prevStatus, newStatus -> + getCurrentStatus(prevStatus, newStatus) + } + } + + private fun Sequence.mapToStatus(): Sequence { + return this.map { walletData -> + when (walletData.status) { + is WalletDataModel.Refreshing -> TotalFiatBalanceStatus.Refreshing + is WalletDataModel.VerifiedOnline, + is WalletDataModel.SameCurrencyTransactionInProgress, + is WalletDataModel.TransactionInProgress, + is WalletDataModel.NoAccount, + -> TotalFiatBalanceStatus.Loaded + is WalletDataModel.Unreachable, + is WalletDataModel.MissedDerivation, + -> TotalFiatBalanceStatus.Error + is WalletDataModel.Loading -> TotalFiatBalanceStatus.Loading + } + } + } + + private fun Sequence.calculateTotalFiatAmount(): BigDecimal { + return this + .map { walletData -> + walletData.fiatRate + ?.let { walletData.status.amount.toFiatValue(it) } + ?: BigDecimal.ZERO + } + .reduce(BigDecimal::plus) + } + + private fun getCurrentStatus( + prevStatus: TotalFiatBalanceStatus, + newStatus: TotalFiatBalanceStatus, + ): TotalFiatBalanceStatus { + return when (prevStatus) { + TotalFiatBalanceStatus.Loading -> prevStatus + TotalFiatBalanceStatus.Refreshing -> when (newStatus) { + TotalFiatBalanceStatus.Loading -> prevStatus + TotalFiatBalanceStatus.Refreshing, + TotalFiatBalanceStatus.Error, + TotalFiatBalanceStatus.Loaded, + -> newStatus + } + TotalFiatBalanceStatus.Loaded, + TotalFiatBalanceStatus.Error, + -> when (newStatus) { + TotalFiatBalanceStatus.Loading, + TotalFiatBalanceStatus.Refreshing, + TotalFiatBalanceStatus.Error, + -> newStatus + TotalFiatBalanceStatus.Loaded -> prevStatus + } + } + } + + private enum class TotalFiatBalanceStatus { + Loading, + Refreshing, + Error, + Loaded, + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt index 05efbdf35f..3dc337ae61 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt @@ -25,5 +25,7 @@ interface UserWalletsListManager { suspend fun delete(walletIds: List): CompletionResult suspend fun clear(): CompletionResult + suspend fun get(walletId: UserWalletId): CompletionResult + companion object } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt similarity index 100% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListProvider.kt rename to app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt index bcc6c05b7f..5135966db1 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt @@ -55,4 +55,10 @@ class DummyUserWalletsListManager : UserWalletsListManager { override suspend fun clear(): CompletionResult { return CompletionResult.Success(Unit) } + + override suspend fun get(walletId: UserWalletId): CompletionResult { + return catching { + error("Not implemented") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresManager.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresManager.kt new file mode 100644 index 0000000000..346a6108e8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresManager.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.domain.walletStores + +import com.tangem.common.CompletionResult +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.model.WalletStoreModel +import kotlinx.coroutines.flow.Flow + +interface WalletStoresManager { + fun getAll(): Flow>> + fun get(userWalletId: UserWalletId): Flow> + + suspend fun delete(userWalletsIds: List): CompletionResult + suspend fun clear(): CompletionResult + + suspend fun fetch( + userWallet: UserWallet, + refresh: Boolean = false, + ): CompletionResult + + suspend fun fetch( + userWallets: List, + refresh: Boolean = false, + ): CompletionResult + + companion object +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/di/WalletsStoresManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/di/WalletsStoresManagerProvider.kt new file mode 100644 index 0000000000..fdc5187641 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/di/WalletsStoresManagerProvider.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.domain.walletStores.di + +import com.tangem.tap.domain.walletStores.WalletStoresManager +import com.tangem.tap.domain.walletStores.implementation.DummyWalletStoresManager + +fun WalletStoresManager.Companion.provideDummyImplementation(): WalletStoresManager { + return DummyWalletStoresManager() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DummyWalletStoresManager.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DummyWalletStoresManager.kt new file mode 100644 index 0000000000..93c5c780c1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DummyWalletStoresManager.kt @@ -0,0 +1,35 @@ +package com.tangem.tap.domain.walletStores.implementation + +import com.tangem.common.CompletionResult +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.model.WalletStoreModel +import com.tangem.tap.domain.walletStores.WalletStoresManager +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +internal class DummyWalletStoresManager : WalletStoresManager { + override fun getAll(): Flow>> { + return emptyFlow() + } + + override fun get(userWalletId: UserWalletId): Flow> { + return emptyFlow() + } + + override suspend fun delete(userWalletsIds: List): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun clear(): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun fetch(userWallet: UserWallet, refresh: Boolean): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun fetch(userWallets: List, refresh: Boolean): CompletionResult { + return CompletionResult.Success(Unit) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 844b55b115..500201104f 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -4,12 +4,12 @@ import com.tangem.common.CompletionResult import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.flatMap -import com.tangem.common.map import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store @@ -65,10 +65,9 @@ internal class SaveWalletMiddleware { ?: return scope.launch { - val userWallet = UserWallet( - scanResponse = scanResponse, - backupCardsIds = state.additionalInfo?.backupCardsIds, - ) + val userWallet = UserWalletBuilder(scanResponse) + .setBackupCardsIds(backupCardsIds = state.additionalInfo?.backupCardsIds) + .build() userWalletsListManager.save(userWallet) .flatMap { @@ -108,14 +107,6 @@ internal class SaveWalletMiddleware { cardsIds = userWallet.cardsInWallet, ) } - userWallet.hasAccessCode -> { - tangemSdkManager - .scanCard( - cardId = userWallet.cardId, - useBiometricsForAccessCode = true, - ) - .map { /* no-op */ } - } else -> { CompletionResult.Success(Unit) } diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/UserWalletModel.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/UserWalletModel.kt new file mode 100644 index 0000000000..a1dc438e52 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/UserWalletModel.kt @@ -0,0 +1,22 @@ +package com.tangem.tap.features.walletSelector.redux + +import com.tangem.tap.domain.model.TotalFiatBalance + +data class UserWalletModel( + val id: String, + val name: String, + val artworkUrl: String, + val type: Type, + val fiatBalance: TotalFiatBalance, +) { + sealed interface Type { + data class SingleCurrency( + val blockchainName: String?, + ) : Type + + data class MultiCurrency( + val tokensCount: Int, + val cardsInWallet: Int, + ) : Type + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt index 51a7253075..d18cf44dec 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt @@ -1,9 +1,33 @@ package com.tangem.tap.features.walletSelector.redux import com.tangem.common.core.TangemError +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.common.entities.FiatCurrency +import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.model.WalletStoreModel import org.rekotlin.Action internal sealed interface WalletSelectorAction : Action { + data class UserWalletsLoaded( + val userWallets: List, + ) : WalletSelectorAction + + data class SelectedWalletChanged( + val selectedWallet: UserWallet, + ) : WalletSelectorAction + + data class IsLockedChanged( + val isLocked: Boolean, + ) : WalletSelectorAction + + data class WalletStoresChanged( + val walletsStores: Map>, + ) : WalletSelectorAction + + data class BalanceLoaded( + val userWalletModel: UserWalletModel, + ) : WalletSelectorAction + object UnlockWithBiometry : WalletSelectorAction { object Success : WalletSelectorAction data class Error(val error: TangemError) : WalletSelectorAction @@ -27,5 +51,10 @@ internal sealed interface WalletSelectorAction : Action { data class Error(val error: TangemError) : WalletSelectorAction } + data class ChangeAppCurrency( + val fiatCurrency: FiatCurrency, + ) : WalletSelectorAction + + data class HandleError(val error: TangemError) : WalletSelectorAction object CloseError : WalletSelectorAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt new file mode 100644 index 0000000000..00f5b77e25 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt @@ -0,0 +1,212 @@ +package com.tangem.tap.features.walletSelector.redux + +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.common.flatMap +import com.tangem.common.map +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.* +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.onUserWalletSelected +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.model.UserWallet +import com.tangem.tap.domain.model.WalletStoreModel +import com.tangem.tap.domain.model.builders.UserWalletBuilder +import com.tangem.tap.domain.scanCard.ScanCardProcessor +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.rekotlin.Middleware +import timber.log.Timber + +internal class WalletSelectorMiddleware { + val middleware: Middleware = { _, appStateProvider -> + { next -> + { action -> + val appState = appStateProvider() + if (action is WalletSelectorAction && appState != null) { + handleAction(action, appState.walletSelectorState) + } + next(action) + } + } + } + + private fun handleAction(action: WalletSelectorAction, state: WalletSelectorState) { + when (action) { + is WalletSelectorAction.UserWalletsLoaded -> { + fetchWalletStores(action.userWallets) + } + is WalletSelectorAction.WalletStoresChanged -> { + updateBalances(action.walletsStores, state) + } + is WalletSelectorAction.UnlockWithBiometry -> { + unlockWalletsWithBiometry() + } + is WalletSelectorAction.AddWallet -> { + addWallet() + } + is WalletSelectorAction.SelectWallet -> { + selectWallet(action.walletId) + } + is WalletSelectorAction.RemoveWallets -> { + removeWallets(action.walletIdsToRemove, state) + } + is WalletSelectorAction.RenameWallet -> { + renameWallet(action.walletId, action.newName) + } + is WalletSelectorAction.ChangeAppCurrency, + is WalletSelectorAction.AddWallet.Success, + is WalletSelectorAction.AddWallet.Error, + is WalletSelectorAction.SelectedWalletChanged, + is WalletSelectorAction.UnlockWithBiometry.Error, + is WalletSelectorAction.UnlockWithBiometry.Success, + is WalletSelectorAction.BalanceLoaded, + is WalletSelectorAction.IsLockedChanged, + is WalletSelectorAction.HandleError, + is WalletSelectorAction.CloseError, + -> Unit + } + } + + private fun fetchWalletStores(userWallets: List) { + scope.launch { + walletStoresManager.fetch(userWallets) + .doOnFailure { error -> + Timber.e(error, "Unable to fetch wallet stores") + store.dispatchOnMain(WalletSelectorAction.HandleError(error)) + } + } + } + + private fun updateBalances(walletStores: Map>, state: WalletSelectorState) { + walletStores.forEach { (walletId, walletStores) -> + scope.launch { + val updatedWallet = state.wallets + .find { it.id == walletId.stringValue } + ?.let { wallet -> + wallet.copy( + type = when (val type = wallet.type) { + is UserWalletModel.Type.MultiCurrency -> type.copy( + tokensCount = walletStores.flatMap { it.walletsData }.size, + ) + is UserWalletModel.Type.SingleCurrency -> type.copy( + blockchainName = walletStores + .firstOrNull() + ?.blockchainNetwork + ?.blockchain + ?.fullName, + ) + }, + fiatBalance = totalFiatBalanceCalculator.calculate( + prevAmount = wallet.fiatBalance.amount, + walletStores = walletStores, + ), + ) + } + + if (updatedWallet != null) { + store.dispatchOnMain(WalletSelectorAction.BalanceLoaded(updatedWallet)) + } + } + } + } + + private fun unlockWalletsWithBiometry() { + scope.launch { + userWalletsListManager.unlockWithBiometry() + .doOnFailure { error -> + store.dispatchOnMain(WalletSelectorAction.UnlockWithBiometry.Error(error)) + } + .doOnSuccess { + store.dispatchOnMain(WalletSelectorAction.UnlockWithBiometry.Success) + } + } + } + + private fun addWallet() = scope.launch { + scanCardInternal { scanResponse -> + val userWallet = UserWalletBuilder(scanResponse).build() + + userWalletsListManager.save(userWallet) + .doOnFailure { error -> + store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error)) + } + .doOnSuccess { + val selectedWallet = userWalletsListManager.selectedUserWallet.first() + val isSavedWalletSelected = userWallet == selectedWallet + store.dispatchOnMain(WalletSelectorAction.AddWallet.Success) + if (isSavedWalletSelected) { + store.dispatchOnMain(NavigationAction.PopBackTo()) + store.onUserWalletSelected(selectedWallet) + } + } + } + } + + private fun selectWallet(id: String) { + scope.launch { + userWalletsListManager.selectWallet(UserWalletId(id)) + .doOnFailure { error -> + store.dispatchOnMain(WalletSelectorAction.HandleError(error)) + } + .doOnSuccess { selectedWallet -> + store.dispatchOnMain(NavigationAction.PopBackTo()) + store.onUserWalletSelected(selectedWallet) + } + } + } + + private fun removeWallets(walletIdsToRemove: List, state: WalletSelectorState) { + scope.launch { + when (walletIdsToRemove.size) { + state.wallets.size -> { // Removing all wallets + userWalletsListManager.clear() + .flatMap { walletStoresManager.clear() } + .doOnSuccess { + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + } + } + + else -> { + userWalletsListManager.delete(walletIdsToRemove.map { UserWalletId(it) }) + .flatMap { walletStoresManager.delete(walletIdsToRemove) } + } + } + .doOnFailure { error -> + store.dispatchOnMain(WalletSelectorAction.HandleError(error)) + } + } + } + + private fun renameWallet(walletId: String, newName: String) { + scope.launch { + userWalletsListManager.get(walletId = UserWalletId(walletId)) + .map { it.copy(name = newName) } + .flatMap { userWalletsListManager.update(it) } + .doOnFailure { error -> + store.dispatchOnMain(WalletSelectorAction.HandleError(error)) + } + } + } + + private suspend inline fun scanCardInternal( + crossinline onCardScanned: suspend (ScanResponse) -> Unit, + ) { + ScanCardProcessor.scan( + useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, + onSuccess = { + onCardScanned(it) + }, + onFailure = { error -> + store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error)) + }, + onWalletNotCreated = { + store.dispatchOnMain(WalletSelectorAction.AddWallet.Success) + store.dispatchOnMain(NavigationAction.PopBackTo()) + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt new file mode 100644 index 0000000000..82fe296fd4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt @@ -0,0 +1,107 @@ +package com.tangem.tap.features.walletSelector.redux + +import com.tangem.domain.common.CardDTO +import com.tangem.tap.common.extensions.replaceByOrAdd +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.domain.extensions.isMultiwalletAllowed +import com.tangem.tap.domain.model.TotalFiatBalance +import com.tangem.tap.domain.model.UserWallet +import org.rekotlin.Action + +internal object WalletSelectorReducer { + fun reduce(action: Action, state: AppState): WalletSelectorState { + return if (action is WalletSelectorAction) { + internalReduce(action, state.walletSelectorState) + } else state.walletSelectorState + } + + private fun internalReduce(action: WalletSelectorAction, state: WalletSelectorState): WalletSelectorState { + return when (action) { + is WalletSelectorAction.UserWalletsLoaded -> state.copy( + wallets = action.userWallets.updateWalletsModels(state.wallets), + ) + is WalletSelectorAction.SelectedWalletChanged -> state.copy( + selectedWalletId = action.selectedWallet.walletId.stringValue, + ) + is WalletSelectorAction.IsLockedChanged -> state.copy( + isLocked = action.isLocked, + ) + is WalletSelectorAction.BalanceLoaded -> state.copy( + wallets = state.wallets.updateWithBalance(action.userWalletModel), + ) + is WalletSelectorAction.HandleError -> state.copy(error = action.error) + is WalletSelectorAction.CloseError -> state.copy(error = null) + is WalletSelectorAction.UnlockWithBiometry -> state.copy( + isUnlockInProgress = true, + ) + is WalletSelectorAction.UnlockWithBiometry.Error -> state.copy( + isUnlockInProgress = false, + error = action.error, + ) + is WalletSelectorAction.UnlockWithBiometry.Success -> state.copy( + isUnlockInProgress = false, + ) + is WalletSelectorAction.AddWallet -> state.copy( + isCardSavingInProgress = true, + ) + is WalletSelectorAction.AddWallet.Error -> state.copy( + isCardSavingInProgress = false, + error = action.error, + ) + is WalletSelectorAction.AddWallet.Success -> state.copy( + isCardSavingInProgress = false, + ) + is WalletSelectorAction.ChangeAppCurrency -> state.copy( + fiatCurrency = action.fiatCurrency, + ) + is WalletSelectorAction.WalletStoresChanged, + is WalletSelectorAction.SelectWallet, + is WalletSelectorAction.RemoveWallets, + is WalletSelectorAction.RenameWallet, + -> state + } + } + + private fun List.updateWalletsModels(prevWallets: List): List { + return this.map { userWallet -> + prevWallets + .find { it.id == userWallet.walletId.stringValue } + ?.copy( + name = userWallet.name, + artworkUrl = userWallet.artworkUrl, + ) + ?: with(userWallet) { + UserWalletModel( + id = walletId.stringValue, + name = name, + artworkUrl = artworkUrl, + type = getType(), + fiatBalance = TotalFiatBalance.Loading, + ) + } + } + } + + private fun List.updateWithBalance( + userWalletModel: UserWalletModel, + ): List { + return ArrayList(this).apply { + replaceByOrAdd(userWalletModel) { it.id == userWalletModel.id } + } + } + + private fun UserWallet.getType(): UserWalletModel.Type { + return if (scanResponse.card.isMultiwalletAllowed) { + UserWalletModel.Type.MultiCurrency( + tokensCount = 0, + cardsInWallet = (scanResponse.card.backupStatus as? CardDTO.BackupStatus.Active) + ?.cardCount?.inc() + ?: 1, + ) + } else { + UserWalletModel.Type.SingleCurrency( + blockchainName = null, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt index c84839277a..df6ec0867d 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt @@ -1,5 +1,15 @@ package com.tangem.tap.features.walletSelector.redux +import com.tangem.common.core.TangemError +import com.tangem.tap.common.entities.FiatCurrency import org.rekotlin.StateType -class WalletSelectorState : StateType \ No newline at end of file +data class WalletSelectorState( + val wallets: List = emptyList(), + val selectedWalletId: String? = null, + val isLocked: Boolean = false, + val fiatCurrency: FiatCurrency = FiatCurrency.Default, + val isCardSavingInProgress: Boolean = false, + val isUnlockInProgress: Boolean = false, + val error: TangemError? = null, +) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/Mapper.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/Mapper.kt new file mode 100644 index 0000000000..34ce8d3ef9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/Mapper.kt @@ -0,0 +1,76 @@ +package com.tangem.tap.features.walletSelector.ui + +import com.tangem.tap.common.entities.FiatCurrency +import com.tangem.tap.common.extensions.toFormattedFiatValue +import com.tangem.tap.domain.model.TotalFiatBalance +import com.tangem.tap.features.details.ui.cardsettings.TextReference +import com.tangem.tap.features.walletSelector.redux.UserWalletModel +import com.tangem.tap.features.walletSelector.redux.WalletSelectorState +import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem +import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem +import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem + +internal fun WalletSelectorScreenState.updateWithNewState( + newState: WalletSelectorState, +): WalletSelectorScreenState { + val walletsUi = newState.wallets.toUiModels(newState.fiatCurrency) + val walletsIds = walletsUi.map { it.id } + val multiCurrencyWallets = arrayListOf() + val singleCurrencyWallets = arrayListOf() + walletsUi.forEach { wallet -> + when (wallet) { + is MultiCurrencyUserWalletItem -> { + multiCurrencyWallets.add(wallet) + } + is SingleCurrencyUserWalletItem -> { + singleCurrencyWallets.add(wallet) + } + } + } + + return this.copy( + multiCurrencyWallets = multiCurrencyWallets, + singleCurrencyWallets = singleCurrencyWallets, + selectedWalletId = newState.selectedWalletId, + editingWalletsIds = editingWalletsIds.filter { it in walletsIds }, + isLocked = newState.isLocked, + showUnlockProgress = newState.isUnlockInProgress, + showAddCardProgress = newState.isCardSavingInProgress, + error = newState.error + ?.takeUnless { it.silent } + ?.let { error -> + error.messageResId?.let { TextReference.Res(it) } + ?: TextReference.Str(error.customMessage) + }, + ) +} + +private fun List.toUiModels( + appCurrency: FiatCurrency, +): Sequence { + return this.asSequence().map { userWalletModel -> + with(userWalletModel) { + val balance = UserWalletItem.Balance( + amount = fiatBalance.amount.toFormattedFiatValue(appCurrency.symbol), + isLoading = fiatBalance is TotalFiatBalance.Loading, + ) + when (type) { + is UserWalletModel.Type.MultiCurrency -> MultiCurrencyUserWalletItem( + id = id, + name = name, + imageUrl = artworkUrl, + balance = balance, + cardsInWallet = type.cardsInWallet, + tokensCount = type.tokensCount, + ) + is UserWalletModel.Type.SingleCurrency -> SingleCurrencyUserWalletItem( + id = id, + name = name, + imageUrl = artworkUrl, + balance = balance, + tokenName = type.blockchainName ?: "—", + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt index 3637f2ea5e..50ee972e7d 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.fragment.app.viewModels import com.tangem.core.ui.fragments.ComposeBottomSheetFragment import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogContent import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent import com.tangem.tap.features.walletSelector.ui.model.RenameWalletDialog @@ -33,7 +34,7 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment + prevState.updateWithNewState(state) + } } override fun onCleared() { @@ -135,10 +144,38 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber + store.dispatchOnMain(WalletSelectorAction.WalletStoresChanged(walletStores)) + } + .launchIn(viewModelScope) + } + + private fun bootstrapAppFiatCurrency() { + store.dispatch( + WalletSelectorAction.ChangeAppCurrency( + fiatCurrency = store.state.globalState.appCurrency, + ), + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index eb7fca3d46..73556a2d6f 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -8,7 +8,7 @@ import com.tangem.tap.common.extensions.onUserWalletSelected 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.model.UserWallet +import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.scope import com.tangem.tap.store @@ -58,7 +58,7 @@ internal class WelcomeMiddleware { private fun proceedWithCard() = scope.launch { scanCardInternal { scanResponse -> - val userWallet = UserWallet(scanResponse) + val userWallet = UserWalletBuilder(scanResponse).build() userWalletsListManager.unlockWithCard(userWallet) .doOnFailure { error ->