Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-24 15:07:35 +03:00
parent d59797b53b
commit 385f8e3f04
5 changed files with 197 additions and 149 deletions

View file

@ -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

View file

@ -178,7 +178,7 @@ internal fun List<WalletDataModel>.updateWithSelf(
val updatedWalletsData = arrayListOf<WalletDataModel>()
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<WalletDataModel>.updateWithSelf(
}
internal fun WalletDataModel.isSameWalletData(other: WalletDataModel): Boolean {
return currency == other.currency
return this.currency == other.currency
}

View file

@ -104,6 +104,9 @@ private inline fun List<WalletStoreModel>.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<WalletStoreModel>.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
}

View file

@ -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<WalletStoreModel>.mapToReduxModels(): List<WalletStore> {
@ -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<WalletDataModel>.mapToReduxModel(
private fun List<WalletDataModel>.mapToReduxModels(
walletRent: WalletStoreModel.WalletRent?,
appCurrencySymbol: String,
): List<WalletData> {
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<WalletDataModel>,
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(

View file

@ -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<WalletData> {
private val walletDataWatcher: ModelWatcher<WalletData> = modelWatcher {
val addressCardStrategy: DiffStrategy<WalletData> = { 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> {
(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<WalletState> = 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<WalletWarning>) = 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)