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 index a0511784e2..7bca1028c0 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt @@ -6,10 +6,12 @@ import com.tangem.common.hdWallet.DerivationPath import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.domain.model.WalletStoreModel.WalletRent import com.tangem.tap.domain.tokens.models.BlockchainNetwork +import com.tangem.tap.features.wallet.models.Currency import java.math.BigDecimal /** * Contains info about the blockchain and its currencies + * * @param userWalletId ID of the associated [UserWallet] * @param blockchain [Blockchain] of this WalletStore * @param derivationPath [DerivationPath] of this store, null if the card does not support the @@ -21,6 +23,9 @@ import java.math.BigDecimal * TODO: Remove after WalletMiddleware refactoring * @param walletManager [WalletManager], may be null if it fails to create this manager. * TODO: Remove after WalletMiddleware refactoring + * + * @property blockchainWalletData Returns the [WalletDataModel] of the blockchain of this wallet store + * or throw [NoSuchElementException] if this wallet store not contains [WalletDataModel] of the blockchain * */ data class WalletStoreModel( val userWalletId: UserWalletId, @@ -34,6 +39,9 @@ data class WalletStoreModel( val walletManager: WalletManager?, ) { + val blockchainWalletData: WalletDataModel + get() = walletsData.first { it.currency is Currency.Blockchain } + /** * Represents wallet blockchain rent * @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt index dc1765ac47..cb49fe33c1 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt @@ -178,7 +178,7 @@ internal fun List.updateWithSelf( val updatedWalletsData = arrayListOf() newWalletsData.forEach { newWalletData -> - val walletDataToUpdate = oldWalletsData.find(newWalletData::isSameWalletData) + val walletDataToUpdate = oldWalletsData.firstOrNull(newWalletData::isSameWalletData) if (walletDataToUpdate != null) { updatedWalletsData.add(walletDataToUpdate.updateWithSelf(newWalletData)) } else { @@ -190,5 +190,5 @@ internal fun List.updateWithSelf( } internal fun WalletDataModel.isSameWalletData(other: WalletDataModel): Boolean { - return currency == other.currency + return this.currency == other.currency } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt index 6679dc7a1d..0c3e47861c 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt @@ -104,6 +104,9 @@ private inline fun List.replaceWalletStores( walletStoresToUpdate.forEach { walletStoreToUpdate -> val index = mutableStores.indexOfFirst(walletStoreToUpdate::isSameWalletStore) + // Can be possible if user hides wallet store when it's tokens is loading + if (index == -1) return@forEach + val currentWalletStore = mutableStores[index] val updatedWalletStore = update(currentWalletStore) @@ -124,6 +127,7 @@ private inline fun List.replaceWalletStores( } internal fun WalletStoreModel.isSameWalletStore(other: WalletStoreModel): Boolean { - return this.blockchain == other.blockchain && + return this.userWalletId == other.userWalletId && + this.blockchain == other.blockchain && this.derivationPath == other.derivationPath } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/Mapper.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/Mapper.kt index 8209e5f65a..155ce6ebd7 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/Mapper.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/Mapper.kt @@ -21,7 +21,6 @@ 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 timber.log.Timber import java.math.BigDecimal internal fun List.mapToReduxModels(): List { @@ -47,100 +46,102 @@ internal fun WalletStoreModel.mapToReduxModel(): WalletStore { return WalletStore( walletManager = walletManager, blockchainNetwork = blockchainNetwork, - walletsData = walletsData.mapToReduxModel(walletRent, appCurrencySymbol), + walletsData = walletsData.mapToReduxModels(walletRent, appCurrencySymbol), ) - .updateTokenModels(blockchain) - .setupIfHadCardSingleToken(blockchain, walletsData, appCurrencySymbol) + .updateTokenModels(blockchainWalletData.status.amount) + .setupIfHadCardSingleToken( + blockchain = blockchain, + walletsDataModel = walletsData, + appCurrencySymbol = appCurrencySymbol, + blockchainWalletData = blockchainWalletData.mapToReduxModel( + walletRent = walletRent, + appCurrencySymbol = appCurrencySymbol, + ), + ) } -private fun List.mapToReduxModel( +private fun List.mapToReduxModels( walletRent: WalletStoreModel.WalletRent?, appCurrencySymbol: String, ): List { return this.map { walletDataModel -> - with(walletDataModel) { - val amount = status.amount - val amountFormatted = amount.toFormattedCurrencyString( - decimals = currency.decimals, - currency = currency.currencySymbol, - ) - val fiatAmount = fiatRate?.let { status.amount.toFiatValue(it) } - val fiatAmountFormatted = fiatAmount - ?.takeIf { !status.isErrorStatus } - ?.toFormattedFiatValue(appCurrencySymbol) - val fiatRateFormatted = fiatRate?.toFiatRateString(appCurrencySymbol) - - WalletData( - currency = currency, - walletAddresses = walletAddresses.getOrNull(0)?.let { selectedAddress -> - WalletAddresses( - selectedAddress = selectedAddress, - list = walletAddresses, - ) - }, - existentialDepositString = existentialDeposit?.toPlainString(), - fiatRate = fiatRate, - fiatRateString = fiatRateFormatted, - pendingTransactions = status.pendingTransactions, - mainButton = WalletMainButton.SendButton( - enabled = !status.amount.isZero() && status.pendingTransactions.isEmpty(), - ), - walletRent = walletRent?.let { - WalletRent( - minRentValue = "${it.rent.stripZeroPlainString()} ${currency.blockchain.currency}", - rentExemptValue = "${it.exemptionAmount.stripZeroPlainString()} ${currency.blockchain.currency}", - ) - }, - currencyData = BalanceWidgetData( - status = when (status) { - is WalletDataModel.Loading -> BalanceStatus.Loading - is WalletDataModel.NoAccount -> BalanceStatus.NoAccount - is WalletDataModel.SameCurrencyTransactionInProgress -> BalanceStatus.SameCurrencyTransactionInProgress - is WalletDataModel.TransactionInProgress -> BalanceStatus.TransactionInProgress - is WalletDataModel.Unreachable -> BalanceStatus.Unreachable - is WalletDataModel.MissedDerivation -> BalanceStatus.MissedDerivation - is WalletDataModel.VerifiedOnline -> BalanceStatus.VerifiedOnline - }, - currency = currency.currencyName, - currencySymbol = currency.currencySymbol, - blockchainAmount = BigDecimal.ZERO, - amount = amount, - amountFormatted = amountFormatted, - fiatAmount = fiatAmount, - fiatAmountFormatted = fiatAmountFormatted, - token = null, - amountToCreateAccount = (status as? WalletDataModel.NoAccount) - ?.amountToCreateAccount - ?.toString(), - errorMessage = status.errorMessage, - ), - ) - } + walletDataModel.mapToReduxModel(walletRent, appCurrencySymbol) } } -private fun WalletStore.updateTokenModels(blockchain: Blockchain): WalletStore { - val foundBlockchains = this.walletsData.filter { - it.currency.isBlockchain() && it.currency.blockchain == blockchain - } - if (foundBlockchains.size != 1) { - val warningMessage = "Can't update information about Tokens in the WalletData list, because " + - "of the WalletStore doesn't contains the Blockchain: %s, or contains more than one" - Timber.w(warningMessage, blockchain.id) - return this - } +private fun WalletDataModel.mapToReduxModel( + walletRent: WalletStoreModel.WalletRent?, + appCurrencySymbol: String, +): WalletData { + val amount = status.amount + val amountFormatted = amount.toFormattedCurrencyString( + decimals = currency.decimals, + currency = currency.currencySymbol, + ) + val fiatAmount = fiatRate?.let { status.amount.toFiatValue(it) } + val fiatAmountFormatted = fiatAmount + ?.takeIf { !status.isErrorStatus } + ?.toFormattedFiatValue(appCurrencySymbol) + val fiatRateFormatted = fiatRate?.toFiatRateString(appCurrencySymbol) - val blockchainAmountValue = foundBlockchains.first().currencyData.blockchainAmount ?: BigDecimal.ZERO + return WalletData( + currency = currency, + walletAddresses = walletAddresses.getOrNull(0)?.let { selectedAddress -> + WalletAddresses( + selectedAddress = selectedAddress, + list = walletAddresses, + ) + }, + existentialDepositString = existentialDeposit?.toPlainString(), + fiatRate = fiatRate, + fiatRateString = fiatRateFormatted, + pendingTransactions = status.pendingTransactions, + mainButton = WalletMainButton.SendButton( + enabled = !status.amount.isZero() && status.pendingTransactions.isEmpty(), + ), + walletRent = walletRent?.let { + WalletRent( + minRentValue = "${it.rent.stripZeroPlainString()} ${currency.blockchain.currency}", + rentExemptValue = "${it.exemptionAmount.stripZeroPlainString()} ${currency.blockchain.currency}", + ) + }, + currencyData = BalanceWidgetData( + status = when (status) { + is WalletDataModel.Loading -> BalanceStatus.Loading + is WalletDataModel.NoAccount -> BalanceStatus.NoAccount + is WalletDataModel.SameCurrencyTransactionInProgress -> BalanceStatus.SameCurrencyTransactionInProgress + is WalletDataModel.TransactionInProgress -> BalanceStatus.TransactionInProgress + is WalletDataModel.Unreachable -> BalanceStatus.Unreachable + is WalletDataModel.MissedDerivation -> BalanceStatus.MissedDerivation + is WalletDataModel.VerifiedOnline -> BalanceStatus.VerifiedOnline + }, + currency = currency.currencyName, + currencySymbol = currency.currencySymbol, + blockchainAmount = BigDecimal.ZERO, + amount = amount, + amountFormatted = amountFormatted, + fiatAmount = fiatAmount, + fiatAmountFormatted = fiatAmountFormatted, + token = null, + amountToCreateAccount = (status as? WalletDataModel.NoAccount) + ?.amountToCreateAccount + ?.toString(), + errorMessage = status.errorMessage, + ), + ) +} + +private fun WalletStore.updateTokenModels(blockchainAmount: BigDecimal): WalletStore { val updatedTokensWalletData = walletsData.filter { it.currency.isToken() }.map { it.copy( mainButton = when (it.mainButton) { is WalletMainButton.SendButton -> { - WalletMainButton.SendButton(it.mainButton.enabled && !blockchainAmountValue.isZero()) + WalletMainButton.SendButton(it.mainButton.enabled && !blockchainAmount.isZero()) } is WalletMainButton.CreateWalletButton -> it.mainButton }, currencyData = it.currencyData.copy( - blockchainAmount = blockchainAmountValue, + blockchainAmount = blockchainAmount, ), ) } @@ -151,17 +152,14 @@ private fun WalletStore.setupIfHadCardSingleToken( blockchain: Blockchain, walletsDataModel: List, appCurrencySymbol: String, + blockchainWalletData: WalletData, ): WalletStore { // Card with single token contains only 2 model - blockchain and token if (walletsData.size != 2) return this - val blockchainWalletData = walletsData.firstOrNull { - it.currency.isBlockchain() && it.currency.blockchain == blockchain - } val cardSingleTokenWalletData = walletsDataModel.firstOrNull { it.currency.isToken() && it.currency.blockchain == blockchain && it.isCardSingleToken - } - if (blockchainWalletData == null || cardSingleTokenWalletData == null) return this + } ?: return this val blockchainWalletDataWithSingleToken = blockchainWalletData.copy( currencyData = blockchainWalletData.currencyData.copy( 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 index e65f402169..e38d9d947b 100644 --- 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 @@ -16,6 +16,8 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding +import com.badoo.mvicore.DiffStrategy +import com.badoo.mvicore.ModelWatcher import com.badoo.mvicore.modelWatcher import com.tangem.common.doOnResult import com.tangem.core.analytics.Analytics @@ -39,6 +41,8 @@ import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.PendingTransaction +import com.tangem.tap.features.wallet.models.WalletWarning +import com.tangem.tap.features.wallet.redux.AddressData import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction @@ -67,7 +71,13 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), private val binding: FragmentWalletDetailsBinding by viewBinding(FragmentWalletDetailsBinding::bind) - private val walletDataWatcher = modelWatcher { + private val walletDataWatcher: ModelWatcher = modelWatcher { + val addressCardStrategy: DiffStrategy = { old, new -> + old.currency != new.currency || + old.walletAddresses?.selectedAddress != new.walletAddresses?.selectedAddress || + old.shouldShowMultipleAddress() != new.shouldShowMultipleAddress() + } + WalletData::pendingTransactions { showPendingTransactionsIfPresent(it) } @@ -77,25 +87,35 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), WalletData::currencyData { setupBalanceData(it) } + WalletData::walletAddresses { walletAddresses -> + setupCopyAndShareButtons(walletAddresses?.selectedAddress?.address) + } + WalletData::assembleWarnings { warnings -> + handleWarnings(warnings) + } (WalletData::currencyData or WalletData::currency) { walletData -> setupCurrency(walletData.currencyData, walletData.currency) setupSwipeRefresh(walletData.currencyData, walletData.currency) } + watch({ it }, addressCardStrategy) { walletData -> + setupAddressCard( + shouldShowMultipleAddress = walletData.shouldShowMultipleAddress(), + selectedAddress = walletData.walletAddresses?.selectedAddress, + currency = walletData.currency, + ) + } } - private val walletStateWatcher = modelWatcher { - (WalletState::selectedCurrency or WalletState::selectedWalletData) { state -> - val selectedWalletData = state.selectedWalletData - if (selectedWalletData != null) { - walletDataWatcher.invoke(selectedWalletData) - setupButtons(selectedWalletData, state.isExchangeServiceFeatureOn) - setupAddressCard(selectedWalletData) - handleWarnings(selectedWalletData) + private val walletStateWatcher: ModelWatcher = modelWatcher { + WalletState::selectedWalletData { selectedWallet -> + if (selectedWallet != null) { + walletDataWatcher.invoke(selectedWallet) } } - (WalletState::selectedCurrency or WalletState::isExchangeServiceFeatureOn) { state -> - if (state.selectedWalletData != null) { - setupButtons(state.selectedWalletData!!, state.isExchangeServiceFeatureOn) + (WalletState::selectedWalletData or WalletState::isExchangeServiceFeatureOn) { state -> + val selectedWallet = state.selectedWalletData + if (selectedWallet != null) { + setupButtonsRow(selectedWallet, state.isExchangeServiceFeatureOn) } } (WalletState::state or WalletState::error) { state -> @@ -132,8 +152,6 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), override fun onStop() { super.onStop() store.unsubscribe(this) - walletDataWatcher.clear() - walletStateWatcher.clear() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -147,6 +165,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), setupTestActionButton() } + override fun onDestroyView() { + super.onDestroyView() + clearWatchers() + } + private fun setupTransactionsRecyclerView() = with(binding) { pendingTransactionAdapter = PendingTransactionsAdapter() rvPendingTransaction.layoutManager = LinearLayoutManager(requireContext()) @@ -251,19 +274,21 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), currencyData.status == BalanceStatus.Refreshing } - private fun setupButtons(selectedWallet: WalletData, isExchangeServiceFeatureOn: Boolean) = with(binding) { - lWalletDetails.btnCopy.setOnClickListener { - selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> - store.dispatch(WalletAction.CopyAddress(addressString, requireContext())) + private fun setupCopyAndShareButtons(walletAddress: String?) { + binding.lWalletDetails.btnCopy.setOnClickListener { + if (walletAddress != null) { + store.dispatch(WalletAction.CopyAddress(walletAddress, requireContext())) } } - lWalletDetails.btnShare.setOnClickListener { - selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> - store.dispatch(WalletAction.ShareAddress(addressString, requireContext())) + binding.lWalletDetails.btnShare.setOnClickListener { + if (walletAddress != null) { + store.dispatch(WalletAction.ShareAddress(walletAddress, requireContext())) } } + } - rowButtons.updateButtonsVisibility( + private fun setupButtonsRow(selectedWallet: WalletData, isExchangeServiceFeatureOn: Boolean) { + binding.rowButtons.updateButtonsVisibility( exchangeServiceFeatureOn = isExchangeServiceFeatureOn, buyAllowed = selectedWallet.isAvailableToBuy, sellAllowed = selectedWallet.isAvailableToSell, @@ -271,9 +296,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), ) } - private fun handleWarnings(selectedWallet: WalletData) = with(binding) { + private fun handleWarnings(warnings: List) = with(binding) { val converter = WalletWarningConverter(requireContext()) - val warningDetails = selectedWallet.assembleWarnings().map { converter.convert(it) } + val warningDetails = warnings.map { converter.convert(it) } warningMessagesAdapter.submitList(warningDetails) rvWarningMessages.show(warningDetails.isNotEmpty()) @@ -294,51 +319,59 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), binding.rvPendingTransaction.show(pendingTransactions.isNotEmpty()) } - private fun setupAddressCard(state: WalletData) = with(binding.lWalletDetails) { - if (state.walletAddresses != null) { - if (state.shouldShowMultipleAddress() && state.currency is Currency.Blockchain) { - (cardBalance as? ViewGroup)?.beginDelayedTransition() - chipGroupAddressType.show() - chipGroupAddressType.fitChipsByGroupWidth() + private fun setupAddressCard( + shouldShowMultipleAddress: Boolean, + selectedAddress: AddressData?, + currency: Currency, + ) = with(binding.lWalletDetails) { + if (selectedAddress == null) return@with - val checkedId = - MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) - if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId) + setupAddressTypeChips(shouldShowMultipleAddress, selectedAddress, currency) - chipGroupAddressType.setOnCheckedChangeListener { group, checkedId -> - if (checkedId == -1) return@setOnCheckedChangeListener - val type = - MultipleAddressUiHelper.idToType(checkedId, state.currency.blockchain) - type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) } - } - } else { - chipGroupAddressType.hide() - } - tvAddress.text = state.walletAddresses.selectedAddress.address - tvExplore.setOnClickListener { - store.dispatch( - WalletAction.ExploreAddress( - state.walletAddresses.selectedAddress.exploreUrl, - requireContext(), - ), - ) - } - ivQrCode.setImageBitmap(state.walletAddresses.selectedAddress.shareUrl.toQrCode()) + tvAddress.text = selectedAddress.address + tvExplore.setOnClickListener { + store.dispatch(WalletAction.ExploreAddress(selectedAddress.exploreUrl, requireContext())) + } + ivQrCode.setImageBitmap(selectedAddress.shareUrl.toQrCode()) - tvReceiveMessage.text = when (val currency = state.currency) { - is Currency.Blockchain -> tvReceiveMessage.getString( - id = R.string.address_qr_code_message_format, - currency.blockchain.fullName, - currency.currencySymbol, - currency.blockchain.fullName, - ) - is Currency.Token -> tvReceiveMessage.getString( - id = R.string.address_qr_code_message_format, - currency.token.name, - currency.currencySymbol, - currency.blockchain.fullName, - ) + tvReceiveMessage.text = when (currency) { + is Currency.Blockchain -> tvReceiveMessage.getString( + id = R.string.address_qr_code_message_format, + currency.blockchain.fullName, + currency.currencySymbol, + currency.blockchain.fullName, + ) + is Currency.Token -> tvReceiveMessage.getString( + id = R.string.address_qr_code_message_format, + currency.token.name, + currency.currencySymbol, + currency.blockchain.fullName, + ) + } + } + + private fun setupAddressTypeChips( + shouldShowMultipleAddress: Boolean, + selectedAddress: AddressData, + currency: Currency, + ) = with(binding.lWalletDetails) { + if (shouldShowMultipleAddress && currency is Currency.Blockchain) { + (cardBalance as? ViewGroup)?.beginDelayedTransition() + chipGroupAddressType.show() + chipGroupAddressType.fitChipsByGroupWidth() + + val checkedId = + MultipleAddressUiHelper.typeToId(selectedAddress.type) + if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId) + + chipGroupAddressType.setOnCheckedChangeListener { _, checkedId -> + if (checkedId == -1) return@setOnCheckedChangeListener + val type = + MultipleAddressUiHelper.idToType(checkedId, currency.blockchain) + type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) } } + } else { + chipGroupAddressType.hide() } } @@ -425,6 +458,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), inflater.inflate(R.menu.menu_wallet_details, menu) } + private fun clearWatchers() { + walletDataWatcher.clear() + walletStateWatcher.clear() + } + private fun TextView.setWarningStatus(mainMessage: Int, error: String? = null) { val text = getString(mainMessage).appendIfNotNull(error, "\nError: ") setStatus(text, R.color.warning, R.drawable.ic_warning_small)