From 8c4ee17b36c97219d55cc118cd2dc155d3671e0a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 21 Jun 2024 20:39:49 +0500 Subject: [PATCH 01/48] Updated on 2026-08-14 --- .../com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 401cdab01a..405897ace0 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -563,6 +563,9 @@ internal class SwapInteractorImpl @Inject constructor( includeFeeInAmount: IncludeFeeInAmount, fee: TxFee, ): SwapTransactionState { + val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError + if (demoConfig.isDemoCardId(cardId)) return SwapTransactionState.DemoMode + return when (swapProvider.type) { ExchangeProviderType.CEX -> { val amountDecimal = toBigDecimalOrNull(amountToSwap) @@ -749,8 +752,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToSend.currency.network.backendId, exchangeDataCex.txExtraId, ) - val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError - if (txExtras == null && exchangeDataCex.txExtraId != null && !demoConfig.isDemoCardId(cardId)) { + if (txExtras == null && exchangeDataCex.txExtraId != null) { return SwapTransactionState.UnknownError } val txData = createTransactionUseCase( From 9d5ba7e3eeaf54b97509f19d0b75acc112a9fe7f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 19:57:22 +0100 Subject: [PATCH 02/48] Updated on 2026-08-14 --- .../swap/domain/models/domain/Warning.kt | 5 +- .../feature/swap/domain/SwapInteractorImpl.kt | 17 +++- .../tangem/feature/swap/models/UiActions.kt | 1 + .../tangem/feature/swap/ui/StateBuilder.kt | 78 +++++++++++++------ .../tangem/feature/swap/ui/TransactionCard.kt | 9 ++- .../feature/swap/viewmodels/SwapViewModel.kt | 5 ++ 6 files changed, 84 insertions(+), 31 deletions(-) diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt index 318b5635de..cc91593e45 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt @@ -4,7 +4,10 @@ import java.math.BigDecimal sealed class Warning { - data class ExistentialDepositWarning(val existentialDeposit: BigDecimal) : Warning() + data class ExistentialDepositWarning( + val existentialDeposit: BigDecimal, + val minAvailableAmount: BigDecimal, + ) : Warning() data class MinAmountWarning(val dustValue: BigDecimal) : Warning() diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 31834e873f..62a0ba68e6 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -376,7 +376,7 @@ internal class SwapInteractorImpl @Inject constructor( val fromToken = fromTokenStatus.currency val userWalletId = getSelectedWallet()?.walletId ?: return emptyList() val warnings = mutableListOf() - manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken) + manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken, feeState) manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount) manageReduceAmountWarning(warnings, fromTokenStatus, amount) return warnings @@ -387,6 +387,7 @@ internal class SwapInteractorImpl @Inject constructor( userWalletId: UserWalletId, amount: SwapAmount, fromToken: CryptoCurrency, + txFee: TxFeeState, ) { val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, fromToken.network) if (existentialDeposit != null) { @@ -394,8 +395,18 @@ internal class SwapInteractorImpl @Inject constructor( fromToken.network.backendId, fromToken.network.derivationPath.value, ) ?: ProxyAmount.empty() - if (nativeBalance.value.minus(amount.value) < existentialDeposit) { - warnings.add(Warning.ExistentialDepositWarning(existentialDeposit)) + // ignore if amount is bigger than balance + if (amount.value > nativeBalance.value) { + return + } + val fee = when (txFee) { + TxFeeState.Empty -> BigDecimal.ZERO + is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue + is TxFeeState.SingleFeeState -> txFee.fee.feeValue + } + val minAvailableAmount = nativeBalance.value - existentialDeposit - fee + if (nativeBalance.value.minus(amount.value + fee) < existentialDeposit) { + warnings.add(Warning.ExistentialDepositWarning(existentialDeposit, minAvailableAmount)) } } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index bd84a05cac..87e4d70044 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -15,6 +15,7 @@ data class UiActions( val onBackClicked: () -> Unit, val onMaxAmountSelected: () -> Unit, val onReduceAmount: (SwapAmount) -> Unit, + val onLeaveExistentialDeposit: (SwapAmount) -> Unit, val onReduceAmountIgnoreClick: () -> Unit, val openPermissionBottomSheet: () -> Unit, val onChangeApproveType: (ApproveType) -> Unit, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index bfb07ed4a0..188ec65b0b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -365,21 +365,7 @@ internal class StateBuilder( quoteModel.warnings.forEach { when (it) { is Warning.ExistentialDepositWarning -> { - warnings.add( - SwapWarning.GeneralInformational( - NotificationConfig( - title = resourceReference(R.string.warning_existential_deposit_title), - subtitle = resourceReference( - R.string.warning_existential_deposit_message, - wrappedList( - quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.name, - it.existentialDeposit.toPlainString(), - ), - ), - iconResId = R.drawable.ic_alert_circle_24, - ), - ), - ) + warnings.add(createLeaveExistentialDepositWarning(quoteModel, it)) } is Warning.MinAmountWarning -> { warnings.add( @@ -444,6 +430,41 @@ internal class StateBuilder( } } + private fun createLeaveExistentialDepositWarning( + quoteModel: SwapState.QuotesLoadedState, + domainWarning: Warning.ExistentialDepositWarning, + ): SwapWarning { + val fromCurrency = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency + val deposit = BigDecimalFormatter.formatCryptoAmountUncapped( + cryptoAmount = domainWarning.existentialDeposit, + cryptoCurrency = fromCurrency, + ) + return SwapWarning.GeneralError( + NotificationConfig( + title = resourceReference(R.string.send_notification_existential_deposit_title), + subtitle = resourceReference( + R.string.send_notification_existential_deposit_text, + wrappedList(deposit), + ), + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference( + R.string.send_notification_leave_button, + wrappedList(deposit), + ), + onClick = { + actions.onLeaveExistentialDeposit( + SwapAmount( + domainWarning.minAvailableAmount, + fromCurrency.decimals, + ), + ) + }, + ), + ), + ) + } + private fun maybeAddPermissionNeededWarning( quoteModel: SwapState.QuotesLoadedState, warnings: MutableList, @@ -469,19 +490,25 @@ internal class StateBuilder( when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { is IncludeFeeInAmount.Included -> { val fee = selectFeeByType(selectedFeeType, quoteModel.txFee) ?: return - warnings.add( - SwapWarning.GeneralWarning( - createNetworkFeeCoverageNotificationConfig( - fee.feeCryptoFormatted, - fee.feeFiatFormatted, + if (needShowNetworkFeeCoverageWarningShow(quoteModel)) { + warnings.add( + SwapWarning.GeneralWarning( + createNetworkFeeCoverageNotificationConfig( + fee.feeCryptoFormatted, + fee.feeFiatFormatted, + ), ), - ), - ) + ) + } } else -> Unit } } + private fun needShowNetworkFeeCoverageWarningShow(quoteModel: SwapState.QuotesLoadedState): Boolean { + return quoteModel.warnings.none { it is Warning.ExistentialDepositWarning } + } + private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): TxFee? { return when (txFeeState) { TxFeeState.Empty -> null @@ -543,7 +570,12 @@ internal class StateBuilder( // check has has outgoing transaction if (preparedSwapConfigState.hasOutgoingTransaction) return false // check has MinAmountWarning warning - if (quoteModel.warnings.filterIsInstance().isNotEmpty()) return false + val hasCriticalWarning = quoteModel.warnings.any { + it is Warning.MinAmountWarning || it is Warning.ExistentialDepositWarning + } + + if (hasCriticalWarning) return false + return when (preparedSwapConfigState.includeFeeInAmount) { IncludeFeeInAmount.BalanceNotEnough -> false IncludeFeeInAmount.Excluded -> diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index bd77a3f66e..e54128fe4d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -283,7 +283,10 @@ private fun Content( if (amountEquivalent != null) { if (type is TransactionCardType.ReadOnly) { - Row(modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20)) { + Row( + modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), + verticalAlignment = Alignment.CenterVertically, + ) { if (priceImpact is PriceImpact.Value) { Text( text = makePriceImpactBalanceWarning( @@ -292,8 +295,6 @@ private fun Content( ), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, - modifier = Modifier - .align(Alignment.CenterVertically), ) } else { AnimatedContent(targetState = amountEquivalent, label = "") { @@ -313,7 +314,7 @@ private fun Content( modifier = Modifier.size(size = TangemTheme.dimens.size20), ) { Icon( - painter = painterResource(id = R.drawable.ic_alert_24), + painter = painterResource(id = R.drawable.ic_information_24), contentDescription = null, tint = if (priceImpact is PriceImpact.Value) { TangemTheme.colors.text.attention diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index d9598de96f..d85a174ead 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -817,6 +817,10 @@ internal class SwapViewModel @Inject constructor( onAmountChanged(newAmount.formatToUIRepresentation()) } + private fun onLeaveExistentialDepositClicked(newAmount: SwapAmount) { + onAmountChanged(newAmount.formatToUIRepresentation()) + } + @Suppress("UnusedPrivateMember") private fun onAmountSelected(selected: Boolean) { if (selected) { @@ -881,6 +885,7 @@ internal class SwapViewModel @Inject constructor( }, onMaxAmountSelected = ::onMaxAmountClicked, onReduceAmount = ::onReduceAmountClicked, + onLeaveExistentialDeposit = ::onLeaveExistentialDepositClicked, onReduceAmountIgnoreClick = { uiState = uiState.copy( reduceAmountIgnore = true, From 1d0d0da8bc8dce041a5f2fb56096e30b4052cc34 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Jun 2024 15:15:29 +0100 Subject: [PATCH 03/48] Updated on 2026-08-14 --- .../java/com/tangem/core/ui/extensions/BlockchainIcons.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 269ec76fd1..33a8bd032e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -59,7 +59,7 @@ fun getActiveIconRes(blockchainId: String): Int { "pls", "pls/test" -> R.drawable.img_pls_22 "zkSyncEra", "zkSyncEra/test" -> R.drawable.img_zksync_22 "moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22 - "manta", "manta/test" -> R.drawable.img_manta_22 + "manta-pacific", "manta/test" -> R.drawable.img_manta_22 "polygonZkEVM", "polygonZkEVM/test" -> R.drawable.img_polygon_22 "moonriver", "moonriver/test" -> R.drawable.img_moonriver_22 "mantle", "mantle/test" -> R.drawable.img_mantle_22 @@ -269,7 +269,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "pls", "pls/test" -> R.drawable.ic_pls_22 "zkSyncEra", "zkSyncEra/test" -> R.drawable.ic_zksync_22 "moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22 - "manta", "manta/test" -> R.drawable.ic_manta_22 + "manta-pacific", "manta/test" -> R.drawable.ic_manta_22 "polygonZkEVM", "polygonZkEVM/test" -> R.drawable.ic_polygon_22 "moonriver", "moonriver/test" -> R.drawable.ic_moonriver_22 "mantle", "mantle/test" -> R.drawable.ic_mantle_22 From 6852c58265b828f241687680943790ad8978ea0f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Jun 2024 11:15:12 +0100 Subject: [PATCH 04/48] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 7d9390b41a..6d75483fe7 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -86,7 +86,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.11-676" +tangemBlockchainSdk = "release-app_5.11-686" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.11-365" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 8882b38a519dde0f761e44fd48da2f26585ef95f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Jun 2024 13:21:24 +0500 Subject: [PATCH 05/48] Updated on 2026-08-14 --- .../presentation/viewmodels/AddCustomTokenViewModel.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index da59bdc238..11efe8bd1f 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -595,13 +595,15 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isTokenAlreadyAdded(): Boolean { + val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain + val networkId = Blockchain.fromNetworkId(networkSelectorValue.toNetworkId())?.id + val contractAddress = convertTokenAddress( + blockchain = networkSelectorValue, + address = uiState.form.contractAddressInputField.value, + ) return currentCryptoCurrencies .filterIsInstance() .any { token -> - val contractAddress = uiState.form.contractAddressInputField.value - val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain - val networkId = Blockchain.fromNetworkId(networkSelectorValue.toNetworkId())?.id - val sameId = if (!token.isCustom) { // todo after move foundToken to CryptoCurrency model, use only id foundToken?.id == token.id.rawCurrencyId From 1277ae6bd273ffb954469471295b1e53cddbf1d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Jun 2024 17:08:10 +0100 Subject: [PATCH 06/48] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/TangemApplication.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 040fb176a2..73e4bcd76f 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -276,6 +276,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { saveBlockchainErrorUseCase = saveBlockchainErrorUseCase, detailsFeatureToggles = detailsFeatureToggles, detailsEntryPoint = detailsEntryPoint, + assetLoader = assetLoader, ), ), ) From 5007d1901f0fc39f5db659236005541d369b3703 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Jun 2024 16:00:16 +0100 Subject: [PATCH 07/48] Updated on 2026-08-14 --- .../tangem/domain/common/CardTypesResolver.kt | 4 ++++ .../domain/common/TangemCardTypesResolver.kt | 16 ++++++++++++++++ .../wallet/domain/WalletImageResolver.kt | 18 ++++++++++++++++++ .../drawable/ill_pastel_cards3_120_106.webp | Bin 0 -> 9576 bytes .../drawable/ill_vivid_cards3_120_106.webp | Bin 0 -> 9308 bytes 5 files changed, 38 insertions(+) create mode 100644 features/wallet/impl/src/main/res/drawable/ill_pastel_cards3_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_vivid_cards3_120_106.webp diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index 0a0a7eee52..55cc073069 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -54,6 +54,10 @@ interface CardTypesResolver { fun isVoltInuWallet(): Boolean + fun isVividWallet(): Boolean + + fun isPastelWallet(): Boolean + fun isWhiteWallet(): Boolean fun isWallet2(): Boolean diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index f43573bf37..82208040e8 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -72,6 +72,14 @@ internal class TangemCardTypesResolver( override fun isVoltInuWallet(): Boolean = card.batchId == VOLT_INU_WALLET_BATCH_ID + override fun isVividWallet(): Boolean = card.batchId == VIVID_LEMON_WALLET_BATCH_ID || + card.batchId == VIVID_AQUA_WALLET_BATCH_ID || + card.batchId == VIVID_GRAPEFRUIT_WALLET_BATCH_ID + + override fun isPastelWallet(): Boolean = card.batchId == PASTEL_PEACH_WALLET_BATCH_ID || + card.batchId == PASTEL_GRASS_WALLET_BATCH_ID || + card.batchId == PASTEL_AIR_WALLET_BATCH_ID + override fun isWhiteWallet(): Boolean { return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } @@ -190,5 +198,13 @@ internal class TangemCardTypesResolver( const val COQ_WALLET_BATCH_ID = "AF28" const val COIN_METRICA_WALLET_BATCH_ID = "AF27" const val VOLT_INU_WALLET_BATCH_ID = "AF35" + // VIVID WALLETS + const val VIVID_LEMON_WALLET_BATCH_ID = "AF40" + const val VIVID_AQUA_WALLET_BATCH_ID = "AF41" + const val VIVID_GRAPEFRUIT_WALLET_BATCH_ID = "AF42" + // PASTEL WALLETS + const val PASTEL_PEACH_WALLET_BATCH_ID = "AF43" + const val PASTEL_AIR_WALLET_BATCH_ID = "AF44" + const val PASTEL_GRASS_WALLET_BATCH_ID = "AF45" } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index a5691979c9..722f7de8f5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -47,6 +47,8 @@ internal object WalletImageResolver { cardTypesResolver.isCOQWallet() -> userWallet.resolveCOQWallet() cardTypesResolver.isCoinMetricaWallet() -> userWallet.resolveCoinMetricaWallet() cardTypesResolver.isVoltInuWallet() -> userWallet.resolveVoltInuWallet() + cardTypesResolver.isVividWallet() -> userWallet.resolveVividWallet() + cardTypesResolver.isPastelWallet() -> userWallet.resolvePastelWallet() cardTypesResolver.isWallet2() -> userWallet.resolveWallet2() cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet() cardTypesResolver.isTangemWallet() -> userWallet.resolveWallet1() @@ -227,6 +229,22 @@ internal object WalletImageResolver { ) } + private fun UserWallet.resolveVividWallet(): Int? { + // for multicolored cards use image of 3 cards in all cases + return resolveWallet2( + oneBackupResId = R.drawable.ill_vivid_cards3_120_106, + twoBackupResId = R.drawable.ill_vivid_cards3_120_106, + ) + } + + private fun UserWallet.resolvePastelWallet(): Int? { + // for multicolored cards use image of 3 cards in all cases + return resolveWallet2( + oneBackupResId = R.drawable.ill_pastel_cards3_120_106, + twoBackupResId = R.drawable.ill_pastel_cards3_120_106, + ) + } + private fun UserWallet.resolveWallet1(): Int? { return resolveWalletWithBackups { count -> when (count) { diff --git a/features/wallet/impl/src/main/res/drawable/ill_pastel_cards3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_pastel_cards3_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b024df94de5d539f962e9077dce76848b7977dc GIT binary patch literal 9576 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 K%kM?BfB*n+-b$tb literal 0 HcmV?d00001 From 75763fb2e0c694322068e7cea5e2e4b67e192c4e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Jun 2024 16:33:29 +0800 Subject: [PATCH 08/48] Updated on 2026-08-14 --- .../tap/common/analytics/events/Settings.kt | 10 ++- .../domain/card/DefaultResetCardUseCase.kt | 26 +++--- .../features/details/redux/DetailsAction.kt | 5 +- .../details/redux/DetailsMiddleware.kt | 8 +- .../features/details/redux/DetailsReducer.kt | 20 +++-- .../features/details/redux/DetailsState.kt | 1 + .../ui/resetcard/ResetCardViewModel.kt | 80 +++++++++++++++---- 7 files changed, 103 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt index b9a831cb3d..8ba8e26539 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt @@ -36,11 +36,19 @@ sealed class Settings( ) : Settings("Settings / Card Settings", event, params, error) { class ButtonFactoryReset : CardSettings("Button - Factory Reset") - class FactoryResetFinished(error: Throwable? = null) : CardSettings( + class FactoryResetFinished(cardsCount: Int? = null, error: Throwable? = null) : CardSettings( event = "Factory Reset Finished", + params = buildMap { + cardsCount?.let { put("Cards Count", "$it") } + }, error = error, ) + class FactoryResetCanceled(cardsCount: Int) : CardSettings( + event = "Factory Reset Canceled", + params = mapOf("Cards Count" to "$cardsCount"), + ) + class UserCodeChanged : CardSettings("User Code Changed") class ButtonChangeSecurityMode : CardSettings("Button - Change Security Mode") diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt index f63f480485..244dd02b7b 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt @@ -1,12 +1,15 @@ package com.tangem.tap.domain.card import arrow.core.Either +import arrow.core.left import arrow.core.raise.either import arrow.core.right -import com.tangem.common.* +import com.tangem.common.CompletionResult +import com.tangem.common.UserCodeType import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.core.UserCodeRequestPolicy +import com.tangem.common.doOnResult import com.tangem.domain.card.ResetCardUseCase import com.tangem.domain.card.models.ResetCardError import com.tangem.domain.models.scan.CardDTO @@ -19,13 +22,12 @@ internal class DefaultResetCardUseCase( override suspend fun invoke(card: CardDTO): Either = either { enterRequiredAccessCode(card) { - tangemSdkManager.resetToFactorySettings( + resetToFactorySettings( cardId = card.cardId, allowsRequestAccessCodeFromRepository = true, ) - .doOnSuccess { Unit.right() } - .doOnFailure { raise(it.mapToDomainError()) } } + .mapToEither() } override suspend fun invoke( @@ -34,20 +36,19 @@ internal class DefaultResetCardUseCase( userWalletId: UserWalletId, ): Either = either { enterRequiredAccessCode(card) { - tangemSdkManager.resetBackupCard(cardNumber, userWalletId) - .doOnSuccess { Unit.right() } - .doOnFailure { raise(it.mapToDomainError()) } + resetBackupCard(cardNumber = cardNumber, userWalletId = userWalletId) } + .mapToEither() } private suspend fun enterRequiredAccessCode( card: CardDTO, task: suspend TangemSdkManager.() -> CompletionResult<*>, - ) { + ): CompletionResult<*> { val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy requestMandatoryAccessCodeEntry(card) - tangemSdkManager.task() + return tangemSdkManager.task() .doOnResult { tangemSdkManager.setUserCodeRequestPolicy(policyBeforeReset) } } @@ -65,6 +66,13 @@ internal class DefaultResetCardUseCase( } } + private fun CompletionResult<*>.mapToEither(): Either { + return when (this) { + is CompletionResult.Failure -> error.mapToDomainError().left() + is CompletionResult.Success -> Unit.right() + } + } + private fun TangemError.mapToDomainError(): ResetCardError { return if (this is TangemSdkError.UserCancelled) ResetCardError.UserCanceled else ResetCardError.AnotherSdkError } 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 cc3efe720c..fa88770744 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 @@ -4,8 +4,6 @@ import androidx.lifecycle.LifecycleCoroutineScope import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode -import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import org.rekotlin.Action @@ -33,7 +31,8 @@ sealed class DetailsAction : Action { data object ScanCard : DetailsAction() - data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction() + data class PrepareCardSettingsData(val scanResponse: ScanResponse) : DetailsAction() + data object ResetCardSettingsData : DetailsAction() data object ScanAndSaveUserWallet : DetailsAction() { 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 372889fe91..c82a82d087 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 @@ -13,7 +13,6 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.TapWorkarounds.isTangemTwins -import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder @@ -147,7 +146,7 @@ class DetailsMiddleware { } .doOnFailure { error -> if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) { - Analytics.send(Settings.CardSettings.FactoryResetFinished(error)) + Analytics.send(Settings.CardSettings.FactoryResetFinished(error = error)) } } .doOnResult { @@ -472,10 +471,7 @@ class DetailsMiddleware { if (isSameWallet) { store.dispatchOnMain( - DetailsAction.PrepareCardSettingsData( - scanResponse.card, - scanResponse.cardTypesResolver, - ), + DetailsAction.PrepareCardSettingsData(scanResponse = scanResponse), ) } else { store.dispatchDialogShow( 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 d8f6637f7f..5a915b21d5 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 @@ -5,6 +5,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.extensions.signedHashesCount @@ -29,11 +30,7 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { handlePrepareScreen(action, state) } is DetailsAction.PrepareCardSettingsData -> { - handlePrepareCardSettingsScreen( - card = action.card, - cardTypesResolver = action.cardTypesResolver, - state = detailsState, - ) + handlePrepareCardSettingsScreen(scanResponse = action.scanResponse, state = detailsState) } is DetailsAction.ResetCardSettingsData -> detailsState.copy(cardSettingsState = null) is DetailsAction.ResetToFactory -> { @@ -97,17 +94,17 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppS ) } -private fun handlePrepareCardSettingsScreen( - card: CardDTO, - cardTypesResolver: CardTypesResolver, - state: DetailsState, -): DetailsState { +private fun handlePrepareCardSettingsScreen(scanResponse: ScanResponse, state: DetailsState): DetailsState { + val cardTypesResolver = scanResponse.cardTypesResolver + val card = scanResponse.card val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2() val isShowPasswordResetRadioButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active + val cardSettingsState = CardSettingsState( cardInfo = card.toCardInfo(cardTypesResolver), + scanResponse = scanResponse, manageSecurityState = prepareSecurityOptions(card, cardTypesResolver), - card = card, + card = scanResponse.card, resetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver), resetButtonEnabled = false, condition1Checked = false, @@ -124,6 +121,7 @@ private fun handlePrepareCardSettingsScreen( isShowPasswordResetRadioButton = isShowPasswordResetRadioButton, dialog = null, ) + return state.copy(cardSettingsState = cardSettingsState) } 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 2c77d5806e..8e7e583ecf 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 @@ -39,6 +39,7 @@ data class AccessCodeRecoveryState( data class CardSettingsState( val cardInfo: CardInfo, val card: CardDTO, + val scanResponse: ScanResponse, val manageSecurityState: ManageSecurityState?, val resetCardAllowed: Boolean, val resetButtonEnabled: Boolean, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index f0628f0cee..fd32427821 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -2,13 +2,21 @@ package com.tangem.tap.features.details.ui.resetcard import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.card.DeleteSavedAccessCodesUseCase import com.tangem.domain.card.ResetCardUseCase +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.features.details.redux.CardSettingsState import com.tangem.tap.features.details.redux.DetailsAction.ResetToFactory @@ -23,6 +31,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject import com.tangem.tap.features.details.redux.CardSettingsState.Dialog as CardSettingsDialog +@Suppress("LongParameterList") @HiltViewModel internal class ResetCardViewModel @Inject constructor( private val resetCardFeatureToggles: ResetCardFeatureToggles, @@ -30,14 +39,17 @@ internal class ResetCardViewModel @Inject constructor( private val resetCardUseCase: ResetCardUseCase, private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, + private val userWalletsListManager: UserWalletsListManager, + private val analyticsEventHandler: AnalyticsEventHandler, ) : ViewModel() { - private val currentUserWallet = getSelectedWalletSyncUseCase().getOrNull() - ?: error("Selected user wallet can't be null") + private val firstCardScanResponse = store.state.detailsState.cardSettingsState?.scanResponse + ?: error("ScanResponse can't be null") + + private val currentUserWalletId = createUserWalletId(firstCardScanResponse) // TODO: move logic to separate domain entity - private val backupCardsCount = (currentUserWallet.getCardsCount() ?: 0) - 1 - private var resetCardsCount = 0 + private var resetBackupCardCount = 0 fun updateState(state: CardSettingsState?): ResetCardScreenState.ResetCardScreenContent { val descriptionText = state?.cardInfo @@ -82,7 +94,7 @@ internal class ResetCardViewModel @Inject constructor( CardSettingsDialog.InterruptedResetDialog -> { ResetCardScreenState.ResetCardScreenContent.Dialog.InterruptedReset( onConfirmClick = ::onContinueResetClick, - onDismiss = ::dismissAndFinishFullReset, + onDismiss = ::onInterruptedResetDialogDismiss, ) } CardSettingsDialog.CompletedResetDialog -> { @@ -105,9 +117,9 @@ internal class ResetCardViewModel @Inject constructor( private fun makeFullReset() { viewModelScope.launch { - resetCardUseCase(card = currentUserWallet.scanResponse.card).onRight { - deleteSavedAccessCodesUseCase(currentUserWallet.cardId) - deleteWalletUseCase(currentUserWallet.walletId) + resetCardUseCase(card = firstCardScanResponse.card).onRight { + deleteSavedAccessCodesUseCase(firstCardScanResponse.card.cardId) + deleteWalletUseCase(currentUserWalletId) val newSelectedWallet = getSelectedWalletSyncUseCase().getOrNull() if (newSelectedWallet != null) { @@ -126,12 +138,12 @@ internal class ResetCardViewModel @Inject constructor( viewModelScope.launch { resetCardUseCase( - cardNumber = resetCardsCount + 1, - card = currentUserWallet.scanResponse.card, - userWalletId = currentUserWallet.walletId, + cardNumber = resetBackupCardCount + 1, + card = firstCardScanResponse.card, + userWalletId = currentUserWalletId, ) .onRight { - resetCardsCount++ + resetBackupCardCount++ delay(DELAY_SDK_DIALOG_CLOSE) @@ -147,10 +159,23 @@ internal class ResetCardViewModel @Inject constructor( showDialog(CardSettingsDialog.InterruptedResetDialog) } + private fun onInterruptedResetDialogDismiss() { + analyticsEventHandler.send(Settings.CardSettings.FactoryResetCanceled(cardsCount = resetBackupCardCount + 1)) + + dismissAndFinishFullReset() + } + private fun checkRemainingBackupCards() { + val backupCardsCount = firstCardScanResponse.getBackupCardsCount() + when { - backupCardsCount > resetCardsCount -> showDialog(CardSettingsDialog.ContinueResetDialog) - backupCardsCount == resetCardsCount -> showDialog(CardSettingsDialog.CompletedResetDialog) + backupCardsCount > resetBackupCardCount -> showDialog(CardSettingsDialog.ContinueResetDialog) + backupCardsCount == resetBackupCardCount -> { + analyticsEventHandler.send( + event = Settings.CardSettings.FactoryResetFinished(cardsCount = resetBackupCardCount + 1), + ) + showDialog(CardSettingsDialog.CompletedResetDialog) + } else -> finishFullReset() } } @@ -162,12 +187,17 @@ internal class ResetCardViewModel @Inject constructor( } private fun finishFullReset() { - val newSelectedWallet = getSelectedWalletSyncUseCase().getOrNull() + val newSelectedWallet = userWalletsListManager.selectedUserWalletSync if (newSelectedWallet != null) { store.dispatch(NavigationAction.PopBackTo(AppScreen.Wallet)) } else { - store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) + val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }.isSuccess + if (isLocked && userWalletsListManager.hasUserWallets) { + store.dispatch(NavigationAction.PopBackTo(AppScreen.Welcome)) + } else { + store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) + } } } @@ -178,4 +208,20 @@ internal class ResetCardViewModel @Inject constructor( private fun dismissDialog() { store.dispatch(ResetToFactory.DismissDialog) } + + private fun ScanResponse.getBackupCardsCount(): Int { + if (!cardTypesResolver.isMultiwalletAllowed()) return 0 + + return when (val status = card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + is CardDTO.BackupStatus.CardLinked -> status.cardCount + is CardDTO.BackupStatus.NoBackup -> 0 + null -> 0 // Multi-currency wallet without backup function. Example, 4.12 + } + } + + private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId { + return UserWalletIdBuilder.scanResponse(scanResponse).build() + ?: error("UserWalletId can't be null") + } } \ No newline at end of file From b460dd5a8fdfde86be40e465dab8cf3f9061b178 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Jun 2024 16:20:37 +0100 Subject: [PATCH 09/48] Updated on 2026-08-14 --- .../java/com/tangem/tap/features/home/redux/HomeMiddleware.kt | 2 +- .../kotlin/com/tangem/features/details/utils/ItemsBuilder.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 17e8d7dd0a..92efb060a0 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -33,7 +33,7 @@ private const val HIDE_PROGRESS_DELAY = 400L object HomeMiddleware { val handler = homeMiddleware - const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/" + const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem&utm_medium=app" } private val homeMiddleware: Middleware = { _, _ -> diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 9bd974e94c..aa18331772 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -100,6 +100,6 @@ internal class ItemsBuilder( ) private companion object { - const val BUY_TANGEM_URL = "https://buy.tangem.com/" + const val BUY_TANGEM_URL = "https://buy.tangem.com/?utm_source=tangem&utm_medium=app" } } \ No newline at end of file From 2ca25eaabed277a1acf3ed68f5a2bc07781d4517 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Jun 2024 18:16:37 +0800 Subject: [PATCH 10/48] Updated on 2026-08-14 --- .../presentation/validators/ContractAddressValidator.kt | 2 +- .../presentation/viewmodels/AddCustomTokenViewModel.kt | 8 ++++++++ gradle/dependencies.toml | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt index 6bb0c880af..966af2bd6a 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt @@ -26,8 +26,8 @@ object ContractAddressValidator { Blockchain.Unknown, Blockchain.Binance, Blockchain.BinanceTestnet, - Blockchain.Cardano, -> SuccessAddressValidator.validate(address) + Blockchain.Cardano -> blockchain.validateContractAddress(address) else -> blockchain.validateAddress(address) } } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 11efe8bd1f..34944cf67a 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token @@ -79,6 +80,7 @@ internal class AddCustomTokenViewModel @Inject constructor( private val testActionsHandler = TestActionsHandler() private val formStateBuilder = FormStateBuilder() private val hederaAddressConverter = HederaTokenAddressConverter() + private val cardanoTokenAddressConverter = CardanoTokenAddressConverter() private var currentCryptoCurrencies: List = emptyList() @@ -898,6 +900,12 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun convertTokenAddress(blockchain: Blockchain, address: String): String { return when (blockchain) { Blockchain.Hedera, Blockchain.HederaTestnet -> hederaAddressConverter.convertToTokenId(address) + Blockchain.Cardano -> { + cardanoTokenAddressConverter.convertToFingerprint( + address = address, + symbol = uiState.form.tokenSymbolInputField.value, + ) + } else -> address } } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index e0d517abb2..88ac8d88ab 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -87,9 +87,9 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.12-689" +tangemBlockchainSdk = "release-app_5.12-691" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.12-364" +tangemCardSdk = "release-app_5.12-367" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem From 2c07a7a771085d4944f410bfe851ce233d29a8e1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Jun 2024 15:50:39 +0500 Subject: [PATCH 11/48] Updated on 2026-08-14 --- .../tangem/tap/di/domain/CardDomainModule.kt | 26 ++++---- .../feature/swap/domain/SwapInteractorImpl.kt | 65 ++++++++++++++----- .../swap/domain/di/SwapDomainModule.kt | 3 +- 3 files changed, 64 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 1a6c5ff684..3d1b711ccf 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -14,15 +14,15 @@ import com.tangem.tap.domain.sdk.TangemSdkManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module -@InstallIn(ViewModelComponent::class) +@InstallIn(SingletonComponent::class) internal object CardDomainModule { @Provides - @ViewModelScoped + @Singleton fun provideGetBiometricsStatusUseCase( cardSdkConfigRepository: CardSdkConfigRepository, ): GetBiometricsStatusUseCase { @@ -30,7 +30,7 @@ internal object CardDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideSetAccessCodeRequestPolicyUseCase( cardSdkConfigRepository: CardSdkConfigRepository, ): SetAccessCodeRequestPolicyUseCase { @@ -38,35 +38,35 @@ internal object CardDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideWasWalletAlreadySignedHashesConfirmedUseCase(cardRepository: CardRepository): WasCardScannedUseCase { return WasCardScannedUseCase(cardRepository = cardRepository) } @Provides - @ViewModelScoped + @Singleton fun provideSetCardWasScannedUseCase(cardRepository: CardRepository): SetCardWasScannedUseCase { return SetCardWasScannedUseCase(cardRepository = cardRepository) } @Provides - @ViewModelScoped + @Singleton fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig()) @Provides - @ViewModelScoped + @Singleton fun provideDerivePublicKeysUseCase(derivationsRepository: DerivationsRepository): DerivePublicKeysUseCase { return DerivePublicKeysUseCase(derivationsRepository = derivationsRepository) } @Provides - @ViewModelScoped + @Singleton fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase { return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager) } @Provides - @ViewModelScoped + @Singleton fun provideGetExtendedPublicKeyForCurrencyUseCase( derivationsRepository: DerivationsRepository, ): GetExtendedPublicKeyForCurrencyUseCase { @@ -74,13 +74,13 @@ internal object CardDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideDeleteSavedAccessCodesUseCase(tangemSdkManager: TangemSdkManager): DeleteSavedAccessCodesUseCase { return DefaultDeleteSavedAccessCodesUseCase(tangemSdkManager) } @Provides - @ViewModelScoped + @Singleton fun provideResetCardUseCase(tangemSdkManager: TangemSdkManager): ResetCardUseCase { return DefaultResetCardUseCase(tangemSdkManager) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index a91c30859e..4a3249cd2c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -13,7 +13,7 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository -import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -65,6 +65,7 @@ internal class SwapInteractorImpl @Inject constructor( private val walletManagersFacade: WalletManagersFacade, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, + private val isDemoCardUseCase: IsDemoCardUseCase, private val quotesRepository: QuotesRepository, private val dispatcher: CoroutineDispatcherProvider, private val swapTransactionRepository: SwapTransactionRepository, @@ -72,7 +73,6 @@ internal class SwapInteractorImpl @Inject constructor( private val appCurrencyRepository: AppCurrencyRepository, private val currenciesRepository: CurrenciesRepository, private val initialToCurrencyResolver: InitialToCurrencyResolver, - private val demoConfig: DemoConfig, private val transactionRepository: TransactionRepository, ) : SwapInteractor { @@ -568,7 +568,7 @@ internal class SwapInteractorImpl @Inject constructor( fee: TxFee, ): SwapTransactionState { val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError - if (demoConfig.isDemoCardId(cardId)) return SwapTransactionState.DemoMode + if (isDemoCardUseCase(cardId)) return SwapTransactionState.DemoMode return when (swapProvider.type) { ExchangeProviderType.CEX -> { @@ -1409,19 +1409,25 @@ internal class SwapInteractorImpl @Inject constructor( swapAmount = swapAmount, spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" }, ) - val feeData = try { - transactionManager.getFee( - networkId = networkId, - amountToSend = BigDecimal.ZERO, - currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)), - destinationAddress = fromToken.getContractAddress(), - increaseBy = INCREASE_GAS_LIMIT_BY, - data = transactionData, - derivationPath = derivationPath, - ) - } catch (e: Exception) { - Timber.e(e, "Failed to get fee") - null + val userWallet = getSelectedWallet() + val cardId = userWallet?.scanResponse?.card?.cardId + val feeData = if (cardId != null && isDemoCardUseCase(cardId)) { + getDemoFees(fromTokenStatus.currency) + } else { + try { + transactionManager.getFee( + networkId = networkId, + amountToSend = BigDecimal.ZERO, + currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)), + destinationAddress = fromToken.getContractAddress(), + increaseBy = INCREASE_GAS_LIMIT_BY, + data = transactionData, + derivationPath = derivationPath, + ) + } catch (e: Exception) { + Timber.e(e, "Failed to get fee") + null + } } val feeState = feeData?.let { when (feeData) { @@ -1805,10 +1811,37 @@ internal class SwapInteractorImpl @Inject constructor( .toMap() } + private fun getDemoFees(cryptoCurrency: CryptoCurrency): ProxyFees.MultipleFees { + val demoFee = ProxyAmount( + currencySymbol = cryptoCurrency.symbol, + value = minDemoFee, + decimals = cryptoCurrency.decimals, + ) + return ProxyFees.MultipleFees( + minFee = ProxyFee.Common( + gasLimit = 1.toBigInteger(), + fee = demoFee, + ), + normalFee = ProxyFee.Common( + gasLimit = 1.toBigInteger(), + fee = demoFee.copy(value = normalDemoFee), + + ), + priorityFee = ProxyFee.Common( + gasLimit = 1.toBigInteger(), + fee = demoFee.copy(value = priorityDemoFee), + ), + ) + } + companion object { @Suppress("UnusedPrivateMember") private const val INCREASE_GAS_LIMIT_BY = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% private const val INFINITY_SYMBOL = "∞" + + private val minDemoFee = "0.0001".toBigDecimal() + private val normalDemoFee = "0.0002".toBigDecimal() + private val priorityDemoFee = "0.0003".toBigDecimal() } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 1a6a6e8b22..a99ef1d6d7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -42,6 +42,7 @@ class SwapDomainModule { getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, @SwapScope sendTransactionUseCase: SendTransactionUseCase, @SwapScope createTransactionUseCase: CreateTransactionUseCase, + isDemoCardUseCase: IsDemoCardUseCase, quotesRepository: QuotesRepository, swapTransactionRepository: SwapTransactionRepository, appCurrencyRepository: AppCurrencyRepository, @@ -61,6 +62,7 @@ class SwapDomainModule { getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, sendTransactionUseCase = sendTransactionUseCase, createTransactionUseCase = createTransactionUseCase, + isDemoCardUseCase = isDemoCardUseCase, quotesRepository = quotesRepository, walletManagersFacade = walletManagersFacade, dispatcher = coroutineDispatcherProvider, @@ -69,7 +71,6 @@ class SwapDomainModule { currencyChecksRepository = currencyChecksRepository, currenciesRepository = currenciesRepository, initialToCurrencyResolver = initialToCurrencyResolver, - demoConfig = DemoConfig(), transactionRepository = transactionRepository, ) } From 9dc776ce21379ebb7a926272842cad2bd72c7820 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jul 2024 15:41:25 +0400 Subject: [PATCH 12/48] Updated on 2026-08-14 --- .../com/tangem/domain/core/lce/LceFlow.kt | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt index 9be30e9699..6204c49430 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt @@ -5,11 +5,11 @@ import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.launch import kotlin.experimental.ExperimentalTypeInference /** @@ -25,35 +25,43 @@ typealias LceFlow = Flow> * It provides methods to handle [Lce] instances and raise errors within a [Flow]. * * @property raise The [LceRaise] instance that this class wraps. - * @property scope The [ProducerScope] that this class operates within. + * @property producerScope The [ProducerScope] instance that this class wraps. * @property ifLoading The function to call if a loading state is raised. */ class LceFlowScope @PublishedApi internal constructor( private val raise: LceRaise, - private val scope: ProducerScope>, + private val producerScope: ProducerScope>, private val ifLoading: suspend LceFlowScope.(C?) -> Unit, -) : Raise, CoroutineScope by scope { +) : Raise, CoroutineScope by producerScope { /** - * Raises an [Lce] instance within the [ProducerScope]. - * It closes the [ProducerScope] after raise. + * Sends a error of type [E] within the [ProducerScope] and then closes it for send. + * All subsequent sends will be ignored. * - * @param r The [Lce] instance to raise. + * This method blocks the coroutine until a error is handled by the receiver. + * + * If the [ProducerScope] is already closed for send (e.g. after rising another error), it just raises [r] + * without closing. + * + * @param r Error to raise. */ override fun raise(r: E): Nothing { - scope.launch(NonCancellable) { - scope.send(r.lceError()) - scope.close() - } + producerScope.trySendBlocking(r.lceError()) + producerScope.close() raise.raise(r.lceError()) } /** - * Sends a content value within the [ProducerScope]. + * Sends a [content] value within the [ProducerScope]. + * * If the content is still loading, it calls [ifLoading] lambda to retrieve a state. * Otherwise, it wraps the content in a [Lce.Content] state. * + * This method suspends until the [Lce] instance is handled by the receiver. + * + * If the [ProducerScope] is closed for send (e.g. after rising a error), it does nothing. + * * @param content The content value to send. * @param isStillLoading A flag indicating whether the content is still loading. */ @@ -65,11 +73,23 @@ class LceFlowScope @PublishedApi internal constructor( content.lceContent() } - scope.send(value) + send(value) } + /** + * Sends an [Lce] instance within the [ProducerScope]. + * + * This method suspends until the [Lce] instance is handled by the receiver. + * + * If the [ProducerScope] is closed for send (e.g. after rising a error), it does nothing. + * + * @param value The [Lce] instance to send. + */ + @OptIn(DelicateCoroutinesApi::class) suspend fun send(value: Lce) { - scope.send(value) + if (producerScope.isClosedForSend) return + + producerScope.send(value) } } @@ -94,7 +114,7 @@ fun lceFlow( lce { val scope = LceFlowScope( raise = this@lce, - scope = this@channelFlow, + producerScope = this@channelFlow, ifLoading = ifLoading, ) From 5f42e8061815403f4814e13a429f2215c0307bfe Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jul 2024 22:37:03 +0800 Subject: [PATCH 13/48] Updated on 2026-08-14 --- .../tasks/UserWalletIdPreflightReadFilter.kt | 2 +- .../details/ui/resetcard/ResetCardViewModel.kt | 7 ++++--- .../wallet/domain/WalletAdditionalInfoFactory.kt | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/UserWalletIdPreflightReadFilter.kt b/app/src/main/java/com/tangem/tap/domain/tasks/UserWalletIdPreflightReadFilter.kt index b0d2d24119..d0f1c97dda 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/UserWalletIdPreflightReadFilter.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/UserWalletIdPreflightReadFilter.kt @@ -18,7 +18,7 @@ class UserWalletIdPreflightReadFilter(private val expectedUserWalletId: UserWall override fun onCardRead(card: Card, environment: SessionEnvironment) = Unit override fun onFullCardRead(card: Card, environment: SessionEnvironment) { - val actualUserWalletId = UserWalletIdBuilder.card(card = CardDTO(card)).build() + val actualUserWalletId = UserWalletIdBuilder.card(card = CardDTO(card)).build() ?: return if (expectedUserWalletId != actualUserWalletId) throw TangemSdkError.WalletNotFound() } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index fd32427821..07f7857126 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -214,9 +214,10 @@ internal class ResetCardViewModel @Inject constructor( return when (val status = card.backupStatus) { is CardDTO.BackupStatus.Active -> status.cardCount - is CardDTO.BackupStatus.CardLinked -> status.cardCount - is CardDTO.BackupStatus.NoBackup -> 0 - null -> 0 // Multi-currency wallet without backup function. Example, 4.12 + is CardDTO.BackupStatus.CardLinked, + is CardDTO.BackupStatus.NoBackup, + null, + -> 0 } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 1f34e460f2..3c61413f63 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo @@ -107,4 +108,18 @@ internal object WalletAdditionalInfoFactory { WalletAdditionalInfo(hideable = true, content = TextReference.Str(value = amount.orEmpty())) } } + + private fun UserWallet.getCardsCount(): Int? { + return if (isMultiCurrency) { + when (val status = scanResponse.card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + 1 + is CardDTO.BackupStatus.CardLinked, + is CardDTO.BackupStatus.NoBackup, + null, + -> 1 + } + } else { + null + } + } } \ No newline at end of file From efc8c003bd1bea8a06c26c1fb7af1418c38dafed Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Jul 2024 21:20:02 +0800 Subject: [PATCH 14/48] Updated on 2026-08-14 --- .../tangem/data/tokens/di/TokensDataModule.kt | 7 +- .../repository/DefaultCurrenciesRepository.kt | 82 ++++++++++--------- .../DefaultMarketCryptoCurrencyRepository.kt | 18 ++-- .../repository/DefaultTxHistoryRepository.kt | 23 +++--- .../DefaultWalletManagersFacade.kt | 63 ++++++++------ 5 files changed, 111 insertions(+), 82 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 062692cd06..f31d0e4b5f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -87,8 +87,11 @@ internal object TokensDataModule { @Provides @Singleton - fun provideDefaultMarketCoinsRepository(assetsStore: AssetsStore): MarketCryptoCurrencyRepository { - return DefaultMarketCryptoCurrencyRepository(assetsStore) + fun provideDefaultMarketCoinsRepository( + assetsStore: AssetsStore, + coroutineDispatcherProvider: CoroutineDispatcherProvider, + ): MarketCryptoCurrencyRepository { + return DefaultMarketCryptoCurrencyRepository(assetsStore, coroutineDispatcherProvider) } @Provides diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 89bb9aca17..9459984032 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -302,26 +302,28 @@ internal class DefaultCurrenciesRepository( networkId: Network.ID, derivationPath: Network.DerivationPath, ): CryptoCurrency.Coin { - val userWallet = getUserWallet(userWalletId) - ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true) + return withContext(dispatchers.io) { + val userWallet = getUserWallet(userWalletId) + ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true) - fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false) + fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false) - val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" + val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + } + val blockchain = Blockchain.fromId(networkId.value) + val blockchainNetworkId = blockchain.toNetworkId() + val coinId = blockchain.toCoinId() + + val storedCoin = storedTokens.tokens + .find { + it.networkId == blockchainNetworkId && it.id == coinId && it.derivationPath == derivationPath.value + } ?: error("Coin in this network $networkId not found") + + val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse) + + coin as? CryptoCurrency.Coin ?: error("Unable to create currency") } - val blockchain = Blockchain.fromId(networkId.value) - val blockchainNetworkId = blockchain.toNetworkId() - val coinId = blockchain.toCoinId() - - val storedCoin = storedTokens.tokens - .find { - it.networkId == blockchainNetworkId && it.id == coinId && it.derivationPath == derivationPath.value - } ?: error("Coin in this network $networkId not found") - - val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse) - - return coin as? CryptoCurrency.Coin ?: error("Unable to create currency") } override fun isTokensGrouped(userWalletId: UserWalletId): Flow { @@ -382,29 +384,31 @@ internal class DefaultCurrenciesRepository( } override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency { - val blockchain = Blockchain.fromId(currency.network.id.value) - return when (val feePaidCurrency = blockchain.feePaidCurrency()) { - FeePaidSdkCurrency.Coin -> FeePaidCurrency.Coin - FeePaidSdkCurrency.SameCurrency -> FeePaidCurrency.SameCurrency - is FeePaidSdkCurrency.Token -> { - val balance = walletManagersFacade.tokenBalance( - userWalletId = userWalletId, - network = currency.network, - name = feePaidCurrency.token.name, - symbol = feePaidCurrency.token.symbol, - contractAddress = feePaidCurrency.token.contractAddress, - decimals = feePaidCurrency.token.decimals, - id = feePaidCurrency.token.id, - ) - FeePaidCurrency.Token( - tokenId = getTokenId(network = currency.network, sdkToken = feePaidCurrency.token), - name = feePaidCurrency.token.name, - symbol = feePaidCurrency.token.symbol, - contractAddress = feePaidCurrency.token.contractAddress, - balance = balance, - ) + return withContext(dispatchers.io) { + val blockchain = Blockchain.fromId(currency.network.id.value) + when (val feePaidCurrency = blockchain.feePaidCurrency()) { + FeePaidSdkCurrency.Coin -> FeePaidCurrency.Coin + FeePaidSdkCurrency.SameCurrency -> FeePaidCurrency.SameCurrency + is FeePaidSdkCurrency.Token -> { + val balance = walletManagersFacade.tokenBalance( + userWalletId = userWalletId, + network = currency.network, + name = feePaidCurrency.token.name, + symbol = feePaidCurrency.token.symbol, + contractAddress = feePaidCurrency.token.contractAddress, + decimals = feePaidCurrency.token.decimals, + id = feePaidCurrency.token.id, + ) + FeePaidCurrency.Token( + tokenId = getTokenId(network = currency.network, sdkToken = feePaidCurrency.token), + name = feePaidCurrency.token.name, + symbol = feePaidCurrency.token.symbol, + contractAddress = feePaidCurrency.token.contractAddress, + balance = balance, + ) + } + is FeePaidSdkCurrency.FeeResource -> FeePaidCurrency.FeeResource(currency = feePaidCurrency.currency) } - is FeePaidSdkCurrency.FeeResource -> FeePaidCurrency.FeeResource(currency = feePaidCurrency.currency) } } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt index a210d96273..d4a0f8fd29 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt @@ -5,9 +5,12 @@ import com.tangem.datasource.local.token.AssetsStore import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext class DefaultMarketCryptoCurrencyRepository( private val assetsStore: AssetsStore, + private val dispatchers: CoroutineDispatcherProvider, ) : MarketCryptoCurrencyRepository { override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { @@ -15,11 +18,16 @@ class DefaultMarketCryptoCurrencyRepository( } private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { - val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE + return withContext(dispatchers.io) { + val contractAddress = + (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE - return assetsStore.getSyncOrNull(userWalletId)?.find { - it.network == cryptoCurrency.network.backendId && - it.contractAddress.equals(contractAddress, ignoreCase = true) - }?.exchangeAvailable ?: false + val asset = assetsStore.getSyncOrNull(userWalletId)?.find { + it.network == cryptoCurrency.network.backendId && + it.contractAddress.equals(contractAddress, ignoreCase = true) + } + + asset?.exchangeAvailable ?: false + } } } \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index fb35baa42f..7d447a5359 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -35,16 +35,19 @@ class DefaultTxHistoryRepository( private val sdkPageConverter by lazy { SdkPageConverter() } override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, currency: CryptoCurrency): Int { - val userWallet = getUserWallet(userWalletId) - val state = walletManagersFacade.getTxHistoryState( - userWalletId = userWallet.walletId, - currency = currency, - ) - return when (state) { - is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception) - is TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented - is TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories - is TxHistoryState.Success.HasTransactions -> state.txCount + return withContext(dispatchers.io) { + val userWallet = getUserWallet(userWalletId) + val state = walletManagersFacade.getTxHistoryState( + userWalletId = userWallet.walletId, + currency = currency, + ) + + when (state) { + is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception) + is TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented + is TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories + is TxHistoryState.Success.HasTransactions -> state.txCount + } } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 5592329e84..25dae1f2dd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -87,11 +87,13 @@ class DefaultWalletManagersFacade( Blockchain.fromId(it.id.value) to it.derivationPath.value } - walletManagersStore.remove(userWalletId) { walletManager -> - val wallet = walletManager.wallet - val blockchainToDerivationPath = wallet.blockchain to wallet.publicKey.derivationPath?.rawPath + withContext(dispatchers.io) { + walletManagersStore.remove(userWalletId) { walletManager -> + val wallet = walletManager.wallet + val blockchainToDerivationPath = wallet.blockchain to wallet.publicKey.derivationPath?.rawPath - blockchainToDerivationPath in blockchainsToDerivationPaths + blockchainToDerivationPath in blockchainsToDerivationPaths + } } } @@ -110,18 +112,20 @@ class DefaultWalletManagersFacade( network: Network, networkTokens: List, ) { - val walletManager = walletManagersStore.getSyncOrNull( - userWalletId = userWalletId, - blockchain = Blockchain.fromId(network.id.value), - derivationPath = network.derivationPath.value, - ) ?: return - val tokensToRemove = sdkTokenConverter.convertList(networkTokens) + withContext(dispatchers.io) { + val walletManager = walletManagersStore.getSyncOrNull( + userWalletId = userWalletId, + blockchain = Blockchain.fromId(network.id.value), + derivationPath = network.derivationPath.value, + ) ?: return@withContext + val tokensToRemove = sdkTokenConverter.convertList(networkTokens) - tokensToRemove.forEach { token -> - walletManager.removeToken(token) + tokensToRemove.forEach { token -> + walletManager.removeToken(token) + } + + walletManagersStore.store(userWalletId, walletManager) } - - walletManagersStore.store(userWalletId, walletManager) } override suspend fun updatePendingTransactions( @@ -576,12 +580,16 @@ class DefaultWalletManagersFacade( userWalletId: UserWalletId, currency: CryptoCurrency, ): AssetRequirementsCondition? { - val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) - val currencyType = cryptoCurrencyTypeConverter.convert(currency) - if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) return null + return withContext(dispatchers.io) { + val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) + val currencyType = cryptoCurrencyTypeConverter.convert(currency) + if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) { + return@withContext null + } - val condition = walletManager.requirementsCondition(currencyType) ?: return null - return requirementsConditionConverter.convert(condition) + val condition = walletManager.requirementsCondition(currencyType) ?: return@withContext null + requirementsConditionConverter.convert(condition) + } } override suspend fun associateAsset( @@ -589,15 +597,18 @@ class DefaultWalletManagersFacade( currency: CryptoCurrency, signer: CommonSigner, ): SimpleResult { - val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) - val currencyType = cryptoCurrencyTypeConverter.convert(currency) + return withContext(dispatchers.io) { + val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) + val currencyType = cryptoCurrencyTypeConverter.convert(currency) - if (walletManager !is AssetRequirementsManager) { - return SimpleResult.Failure( - BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"), - ) + if (walletManager !is AssetRequirementsManager) { + return@withContext SimpleResult.Failure( + BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"), + ) + } + + walletManager.fulfillRequirements(currencyType, signer) } - return walletManager.fulfillRequirements(currencyType, signer) } override suspend fun checkUtxoConsolidationAvailability(userWalletId: UserWalletId, network: Network): Boolean { From 26d0b1c624b967b16274e7109d9178b0600b0ac6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jul 2024 19:40:34 +0100 Subject: [PATCH 15/48] Updated on 2026-08-14 --- .../models/response/ExchangeProvider.kt | 3 + .../swap/converters/SwapPairInfoConverter.kt | 1 + .../domain/models/domain/SwapPairLeast.kt | 7 ++- .../feature/swap/domain/SwapInteractorImpl.kt | 59 +++++++++++-------- .../tangem/feature/swap/ui/StateBuilder.kt | 12 ++-- .../feature/swap/ui/SwapSuccessScreen.kt | 2 +- .../exchange/ExchangeStatusBottomSheet.kt | 2 +- 7 files changed, 51 insertions(+), 35 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt index acbd56ec03..8b224858c1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt @@ -31,4 +31,7 @@ enum class ExchangeProviderType { @Json(name = "cex") CEX, + + @Json(name = "dex-bridge") + DEX_BRIDGE, } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index 5bb5079f95..cdaa277dac 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -68,6 +68,7 @@ class SwapPairInfoConverter : Converter ExchangeProviderTypeDomain.DEX ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX + ExchangeProviderType.DEX_BRIDGE -> ExchangeProviderTypeDomain.DEX_BRIDGE } } } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index 9df61116fa..1cbcdf41d1 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -36,9 +36,10 @@ data class SwapProvider( val privacyPolicy: String?, ) -enum class ExchangeProviderType { - DEX, - CEX, +enum class ExchangeProviderType(val providerName: String) { + DEX("DEX"), + CEX("CEX"), + DEX_BRIDGE("DEX/Bridge"), } /** diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 4a3249cd2c..eb1639b60d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -268,7 +268,7 @@ internal class SwapInteractorImpl @Inject constructor( val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null) val networkId = fromToken.currency.network.backendId when (provider.type) { - ExchangeProviderType.DEX -> { + ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { manageDex( networkId = networkId, fromToken = fromToken, @@ -342,7 +342,7 @@ internal class SwapInteractorImpl @Inject constructor( ) } else { provider to getQuotesState( - exchangeProviderType = ExchangeProviderType.DEX, + exchangeProviderType = provider.type, quoteDataModel = quotes, amount = amount, fromToken = fromToken, @@ -588,8 +588,9 @@ internal class SwapInteractorImpl @Inject constructor( userWalletId = requireNotNull(getSelectedWallet()).walletId, ) } - ExchangeProviderType.DEX -> { + ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { onSwapDex( + provider = swapProvider, networkId = currencyToSend.currency.network.backendId, swapData = requireNotNull(swapData), currencyToSendStatus = currencyToSend, @@ -641,6 +642,7 @@ internal class SwapInteractorImpl @Inject constructor( } private suspend fun onSwapDex( + provider: SwapProvider, networkId: String, swapData: SwapDataModel, currencyToSendStatus: CryptoCurrencyStatus, @@ -652,7 +654,8 @@ internal class SwapInteractorImpl @Inject constructor( val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals) val derivationPath = currencyToSendStatus.currency.network.derivationPath.value - val dataToSign = (swapData.transaction as ExpressTransactionModel.DEX).txData + val dexTransaction = swapData.transaction as ExpressTransactionModel.DEX + val dataToSign = dexTransaction.txData val txData = createTransactionUseCase( amount = amount.value.convertToAmount(currencyToSendStatus.currency), fee = getFeeForTransaction( @@ -686,6 +689,17 @@ internal class SwapInteractorImpl @Inject constructor( txHash = txHash, payInExtraId = swapData.transaction.txExtraId, ) + if (provider.type == ExchangeProviderType.DEX_BRIDGE) { + val timestamp = System.currentTimeMillis() + storeSwapTransaction( + currencyToSend = currencyToSendStatus, + currencyToGet = currencyToGetStatus, + amount = amount, + swapProvider = provider, + swapDataModel = swapData, + timestamp = timestamp, + ) + } storeLastCryptoCurrencyId(currencyToGetStatus.currency) SwapTransactionState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( @@ -702,16 +716,7 @@ internal class SwapInteractorImpl @Inject constructor( timestamp = System.currentTimeMillis(), ) }, - ifLeft = { - when (it) { - SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled - is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError - is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError - is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError - is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode - else -> SwapTransactionState.UnknownError - } - }, + ifLeft = { handleSendTxError(it) }, ) } @@ -783,14 +788,7 @@ internal class SwapInteractorImpl @Inject constructor( val derivationPath = currencyToSend.currency.network.derivationPath.value return result.fold( ifLeft = { - when (it) { - SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled - is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError - is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError - is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError - is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode - else -> SwapTransactionState.UnknownError - } + handleSendTxError(it) }, ifRight = { txHash -> repository.exchangeSent( @@ -836,6 +834,17 @@ internal class SwapInteractorImpl @Inject constructor( ) } + private fun handleSendTxError(txError: SendTransactionError?): SwapTransactionState { + return when (txError) { + SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled + is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError + is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError + is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError + is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode + else -> SwapTransactionState.UnknownError + } + } + private fun getFeeForTransaction(fee: TxFee, blockchain: Blockchain): Fee { val feeAmountValue = fee.feeValue val feeAmount = Amount( @@ -884,8 +893,8 @@ internal class SwapInteractorImpl @Inject constructor( swapProvider: SwapProvider, swapDataModel: SwapDataModel, timestamp: Long, - txExternalUrl: String, - txExternalId: String, + txExternalUrl: String? = null, + txExternalId: String? = null, ) { swapTransactionRepository.storeTransaction( userWalletId = UserWalletId(userWalletManager.getWalletId()), @@ -1074,7 +1083,7 @@ internal class SwapInteractorImpl @Inject constructor( ) when (exchangeProviderType) { - ExchangeProviderType.DEX -> { + ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { val state = updatePermissionState( networkId = networkId, fromTokenStatus = fromToken, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index c902406819..880518dcdb 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1004,13 +1004,15 @@ internal class StateBuilder( val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount)) val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount)) + val shouldShowStatus = providerState.type == ExchangeProviderType.CEX.providerName || + providerState.type == ExchangeProviderType.DEX_BRIDGE.providerName return uiState.copy( successState = SwapSuccessStateHolder( timestamp = swapTransactionState.timestamp, txUrl = txUrl, providerName = stringReference(providerState.name), providerType = stringReference(providerState.type), - showStatusButton = providerState.type == ExchangeProviderType.CEX.name, + showStatusButton = shouldShowStatus, providerIcon = providerState.iconUrl, rate = providerState.subtitle, fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"), @@ -1092,7 +1094,7 @@ internal class StateBuilder( ): SwapStateHolder { val message = when (providerType) { ExchangeProviderType.CEX -> resourceReference(R.string.swapping_alert_cex_description, wrappedList(token)) - ExchangeProviderType.DEX -> { + ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { val refs = buildList { if (isPriceImpact) { add(resourceReference(R.string.swapping_high_price_impact_description)) @@ -1548,7 +1550,7 @@ internal class StateBuilder( id = this.providerId, name = this.name, iconUrl = this.imageLarge, - type = this.type.toString(), + type = this.type.providerName, subtitle = stringReference(rateString), additionalBadge = badge, selectionType = selectionType, @@ -1575,7 +1577,7 @@ internal class StateBuilder( id = this.providerId, name = this.name, iconUrl = this.imageLarge, - type = this.type.toString(), + type = this.type.providerName, subtitle = stringReference(rateString), additionalBadge = additionalBadge, selectionType = selectionType, @@ -1612,7 +1614,7 @@ internal class StateBuilder( id = this.providerId, name = this.name, iconUrl = this.imageLarge, - type = this.type.toString(), + type = this.type.providerName, selectionType = selectionType, subtitle = alertText, additionalBadge = ProviderState.AdditionalBadge.Empty, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index 11ade5db09..6f1b0517ff 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -154,7 +154,7 @@ private val state = SwapSuccessStateHolder( txUrl = "https://www.google.com/#q=nam", fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), providerName = TextReference.Str("1inch"), - providerType = TextReference.Str(ExchangeProviderType.DEX.name), + providerType = TextReference.Str(ExchangeProviderType.DEX.providerName), showStatusButton = false, providerIcon = "", fromTokenAmount = TextReference.Str("1 000 DAI"), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index 1df5e537c1..a1a7c595a5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -70,7 +70,7 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC SpacerH12() ExchangeProvider( providerName = TextReference.Str(config.provider.name), - providerType = TextReference.Str(config.provider.type.name), + providerType = TextReference.Str(config.provider.type.providerName), providerTxId = config.txExternalId, imageUrl = config.provider.imageLarge, ) From f2c6c7be527e4184bcefe97cf29f84479d044b92 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jul 2024 19:40:54 +0100 Subject: [PATCH 16/48] Updated on 2026-08-14 --- .../models/response/ExchangeStatusResponse.kt | 29 ++++++--- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + .../domain/models/domain/ExchangeStatus.kt | 2 + ...enDetailsSwapTransactionsStateConverter.kt | 65 +++++++++++++------ .../viewmodels/ExchangeStatusFactory.kt | 1 + 6 files changed, 68 insertions(+), 31 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt index a269f45c23..dc786b02de 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt @@ -23,34 +23,43 @@ data class ExchangeStatusResponse( enum class ExchangeStatus { @Json(name = "new") - NEW, + New, @Json(name = "waiting") - WAITING, + Waiting, @Json(name = "confirming") - CONFIRMING, + Confirming, @Json(name = "exchanging") - EXCHANGING, + Exchanging, @Json(name = "sending") - SENDING, + Sending, @Json(name = "finished") - FINISHED, + Finished, @Json(name = "failed") - FAILED, + Failed, @Json(name = "refunded") - REFUNDED, + Refunded, @Json(name = "verifying") - VERIFYING, + Verifying, @Json(name = "expired") - CANCELLED, + Cancelled, + + @Json(name = "waiting-tx-hash") + WaitingTxHash, + + @Json(name = "tx-failed") + TxFailed, + + @Json(name = "unknown") + Unknown, } data class ExchangeStatusError( diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 0c2f9e38e2..4772a93753 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -225,6 +225,7 @@ Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий. Статус обмена Требуется верификация + Ожидание хеша транзакции Список токенов в вашем кошельке Получение наилучших курсов... Плавающая ставка diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 0e761dde1b..6a39461d9a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -224,6 +224,7 @@ Provider-sourced data. Estimated amount subject to change due to market conditions. Exchange status Verification required + Awaiting transaction hash List of all tokens added to your wallet Fetching best rates... Floating rate diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt index a422c02284..c52f37ce6c 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt @@ -11,6 +11,7 @@ data class ExchangeStatusModel( enum class ExchangeStatus { New, Waiting, + WaitingTxHash, Confirming, Verifying, Exchanging, @@ -19,4 +20,5 @@ enum class ExchangeStatus { Finished, Refunded, Cancelled, + Unknown, } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 55e6cba9b0..6c1a71f7af 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -162,6 +162,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): ImmutableList { if (status == null) return persistentListOf() val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting + val isWaitingTxHash = status == ExchangeStatus.WaitingTxHash val isConfirming = status == ExchangeStatus.Confirming val isVerifying = status == ExchangeStatus.Verifying val isExchanging = status == ExchangeStatus.Exchanging @@ -174,27 +175,35 @@ internal class TokenDetailsSwapTransactionsStateConverter( val isExchangingDone = !isExchanging && isConfirmingDone val isSendingDone = !isSending && !isVerifying && !isFailed && isExchangingDone - return if (status == ExchangeStatus.Cancelled) { - listOf(cancelledStep()) - } else { - listOf( - waitStep(isWaiting, isWaitingDone), - confirmStep(isConfirming, isConfirmingDone), - exchangeStep( - isExchanging = isExchanging, - isExchangingDone = isExchangingDone, - isRefunded = isRefunded, - hasFailed = hasFailed, - isVerifying = isVerifying, - isFailed = isFailed, - ), - sendStep( - isSending = isSending, - isSendingDone = isSendingDone, - isRefunded = isRefunded, - hasFailed = hasFailed, - ), - ) + return buildList { + when { + status == ExchangeStatus.Cancelled -> add(cancelledStep()) + isWaitingTxHash -> add(waitTxStep()) + // ExchangeStatus.Unknown is temporary added for 1inch + status == ExchangeStatus.Unknown -> add(unknownStateStep()) + else -> { + add(waitStep(isWaiting, isWaitingDone)) + add(confirmStep(isConfirming, isConfirmingDone)) + add( + exchangeStep( + isExchanging = isExchanging, + isExchangingDone = isExchangingDone, + isRefunded = isRefunded, + hasFailed = hasFailed, + isVerifying = isVerifying, + isFailed = isFailed, + ), + ) + add( + sendStep( + isSending = isSending, + isSendingDone = isSendingDone, + isRefunded = isRefunded, + hasFailed = hasFailed, + ), + ) + } + } }.toPersistentList() } @@ -216,6 +225,20 @@ internal class TokenDetailsSwapTransactionsStateConverter( isDone = isNewDone, ) + private fun waitTxStep() = ExchangeStatusState( + status = ExchangeStatus.Verifying, + text = TextReference.Res(R.string.express_exchange_status_waiting_tx_hash), + isActive = false, + isDone = false, + ) + + private fun unknownStateStep() = ExchangeStatusState( + status = ExchangeStatus.Failed, + text = TextReference.Res(R.string.express_exchange_status_failed), + isActive = false, + isDone = true, + ) + private fun confirmStep(isConfirming: Boolean, isConfirmingDone: Boolean) = ExchangeStatusState( status = ExchangeStatus.Confirming, text = when { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index 4bc8e7fb37..e77952c869 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -163,6 +163,7 @@ internal class ExchangeStatusFactory( return when (status) { ExchangeStatus.New, ExchangeStatus.Waiting, + ExchangeStatus.WaitingTxHash, ExchangeStatus.Sending, ExchangeStatus.Confirming, ExchangeStatus.Exchanging, From 45b0da57f447e085a32e5c8b65ff17ad97e5dcd8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 01:54:33 +0800 Subject: [PATCH 17/48] Updated on 2026-08-14 --- .../wallet/subscribers/BasicTokenListSubscriber.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index c9bd3afc92..61d278b4d0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -74,7 +74,9 @@ internal abstract class BasicTokenListSubscriber( }, ifError = { e -> Timber.e("Failed to load token list: $e") - SetTokenListErrorTransformer(userWallet.walletId, e) + stateHolder.update( + SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = e), + ) return@combine }, ) From 540f27d1c4f3d03c645b0be8bd7bcbfca8b490db Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 21:03:27 +0800 Subject: [PATCH 18/48] Updated on 2026-08-14 --- .../SetTokenListErrorTransformer.kt | 35 ++++++++++++++++--- .../subscribers/BasicTokenListSubscriber.kt | 16 +++++---- .../viewmodels/intents/WalletClickIntents.kt | 11 +++++- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index 046b5fd498..fd70fb3cfa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -1,22 +1,32 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState import timber.log.Timber +import java.math.BigDecimal internal class SetTokenListErrorTransformer( - userWalletId: UserWalletId, + private val selectedWallet: UserWallet, private val error: TokenListError, -) : WalletStateTransformer(userWalletId) { + private val appCurrency: AppCurrency, +) : WalletStateTransformer(selectedWallet.walletId) { override fun transform(prevState: WalletState): WalletState { return when (error) { is TokenListError.EmptyTokens -> { when (prevState) { is WalletState.MultiCurrency.Content -> { - prevState.copy(tokensListState = WalletTokensListState.Empty) + prevState.copy( + walletCardState = prevState.walletCardState.toLoadedState(), + tokensListState = WalletTokensListState.Empty, + ) } is WalletState.MultiCurrency.Locked -> { Timber.w("Impossible to load tokens list for locked wallet") @@ -37,4 +47,21 @@ internal class SetTokenListErrorTransformer( -> prevState } } + + private fun WalletCardState.toLoadedState(): WalletCardState { + return WalletCardState.Content( + id = id, + title = title, + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = selectedWallet), + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + balance = BigDecimalFormatter.formatFiatAmount( + fiatAmount = BigDecimal.ZERO, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + cardCount = selectedWallet.getCardsCount(), + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index 61d278b4d0..f074661604 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -61,6 +61,11 @@ internal abstract class BasicTokenListSubscriber( }, flow2 = getSelectedAppCurrencyUseCase().distinctUntilChanged(), transform = { maybeTokenList, maybeAppCurrency -> + val appCurrency = maybeAppCurrency.getOrElse { e -> + Timber.e("Failed to load app currency: $e") + AppCurrency.Default + } + val tokenList = maybeTokenList.getOrElse( ifLoading = { maybeContent -> val isRefreshing = stateHolder.getWalletState(userWallet.walletId) @@ -75,17 +80,16 @@ internal abstract class BasicTokenListSubscriber( ifError = { e -> Timber.e("Failed to load token list: $e") stateHolder.update( - SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = e), + SetTokenListErrorTransformer( + selectedWallet = userWallet, + error = e, + appCurrency = appCurrency, + ), ) return@combine }, ) - val appCurrency = maybeAppCurrency.getOrElse { e -> - Timber.e("Failed to load app currency: $e") - AppCurrency.Default - } - updateContent(tokenList, appCurrency) walletWithFundsChecker.check(tokenList) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index 3266406422..eba25e8d34 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -1,6 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.settings.NeverToShowWalletsScrollPreview import com.tangem.domain.tokens.FetchCardTokenListUseCase @@ -37,6 +39,7 @@ internal class WalletClickIntents @Inject constructor( private val fetchTokenListUseCase: FetchTokenListUseCase, private val fetchCardTokenListUseCase: FetchCardTokenListUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview, private val analyticsEventHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, @@ -115,7 +118,13 @@ internal class WalletClickIntents @Inject constructor( } maybeFetchResult.onLeft { - stateHolder.update(SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it)) + stateHolder.update( + SetTokenListErrorTransformer( + selectedWallet = userWallet, + error = it, + appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + ), + ) } stateHolder.update( From 353efc2c213f6c22a4c335448ad0fa1a1e3d7ce4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 18:36:02 +0500 Subject: [PATCH 19/48] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 1 + .../local/preferences/PreferencesKeys.kt | 16 +- core/res/src/main/res/values-ru/strings.xml | 2 + core/res/src/main/res/values/strings.xml | 2 + .../notifications/OkxPromoNotification.kt | 163 ++++++++++++++++++ .../com/tangem/core/ui/res/TangemColors.kt | 2 + .../main/res/drawable/img_okx_dex_logo.xml | 29 ++++ .../data/promo/DefaultPromoRepository.kt | 10 ++ .../DefaultPromoSettingsRepository.kt | 12 +- .../analytics/TokenSwapPromoAnalyticsEvent.kt | 41 ++++- .../tokens/GetCurrencyWarningsUseCase.kt | 2 +- .../tokens/repository/PromoRepository.kt | 2 + ...okenDetailsNotificationsAnalyticsSender.kt | 7 +- .../components/TokenDetailsNotification.kt | 18 +- .../tokendetails/ui/TokenDetailsScreen.kt | 4 +- .../viewmodels/TokenDetailsViewModel.kt | 17 +- .../analytics/WalletScreenAnalyticsEvent.kt | 34 ---- .../utils/WalletWarningsAnalyticsSender.kt | 11 +- .../domain/GetMultiWalletWarningsFactory.kt | 22 ++- .../wallet/state/model/WalletNotification.kt | 18 +- .../components/common/WalletNotifications.kt | 4 +- .../intents/WalletWarningsClickIntents.kt | 21 ++- 22 files changed, 329 insertions(+), 109 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt create mode 100644 core/ui/src/main/res/drawable/img_okx_dex_logo.xml diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index b77078a426..c7c73612f8 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -64,6 +64,7 @@ sealed class AnalyticsParam { data object Send : ScreensSources("Send") data object Intro : ScreensSources("Introduction") data object MyWallets : ScreensSources("My Wallets") + data object Token : ScreensSources("Token") } sealed class TxSentFrom(val value: String) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index e069422752..52b037fd6d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -53,14 +53,6 @@ object PreferencesKeys { val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") } - val IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY by lazy { - booleanPreferencesKey(name = "isWalletSwapPromoChangellyShown") - } - - val IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY by lazy { - booleanPreferencesKey(name = "isTokenSwapPromoChangellyShown") - } - val IS_WALLET_TRAVALA_PROMO_SHOWN_KEY by lazy { booleanPreferencesKey(name = "isWalletTravalaPromoShown") } @@ -93,6 +85,14 @@ object PreferencesKeys { val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") } + val IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY by lazy { + booleanPreferencesKey(name = "isWalletSwapPromoOkxShown") + } + + val IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY by lazy { + booleanPreferencesKey(name = "isTokenSwapPromoOkxShown") + } + fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region") } diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 4772a93753..453bf3dd9b 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -550,6 +550,8 @@ Встречайте Tangem Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах Поддержка Web 3.0 + Обменивайте больше токенов по лучшим курсам прямо в вашем кошельке. + Новый провайдер обмена! В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя В сумму включена комиссия провайдера сервиса. Комиссии diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 6a39461d9a..c712fdab72 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -543,6 +543,8 @@ Meet Tangem Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services Web 3.0 Compatible + Exchange more tokens at better rates directly in your wallet. + New Swap Provider Available! The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address. The amount includes the service provider\'s fee. Fees diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt new file mode 100644 index 0000000000..9d6d403946 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt @@ -0,0 +1,163 @@ +package com.tangem.core.ui.components.notifications + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.R +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonColors +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemColorPalette.Dark6 +import com.tangem.core.ui.res.TangemColorPalette.Light4 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +private val OxkPromoColor = Color(0xFFBCFF2F) + +@Composable +fun OkxPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) { + Column( + modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(Dark6), + ) { + Content(config = config) + Button(config = config) + } +} + +@Composable +private fun Content(config: NotificationConfig) { + Row { + Icon( + painter = rememberVectorPainter(image = ImageVector.vectorResource(id = R.drawable.img_okx_dex_logo)), + contentDescription = null, + tint = TangemTheme.colors.icon.constant, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12) + .align(Alignment.CenterVertically), + ) + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + modifier = Modifier + .weight(1f) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = config.title.resolveReference(), + style = TangemTheme.typography.button, + color = OxkPromoColor, + ) + Text( + text = config.subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.constantWhite, + ) + } + config.onCloseClick?.let { + Icon( + painter = rememberVectorPainter( + image = ImageVector.vectorResource(R.drawable.ic_close_24), + ), + contentDescription = null, + tint = TangemTheme.colors.icon.constant, + modifier = + Modifier + .padding( + top = TangemTheme.dimens.spacing8, + end = TangemTheme.dimens.spacing8, + ) + .size(TangemTheme.dimens.size20) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(radius = TangemTheme.dimens.radius10), + onClick = it, + ), + ) + } + } +} + +@Composable +private fun Button(config: NotificationConfig) { + val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig + + button?.let { + val isDarkMode = LocalIsInDarkTheme.current + TangemButton( + text = button.text.resolveReference(), + icon = TangemButtonIconPosition.Start(button.iconResId ?: R.drawable.ic_exchange_vertical_24), + onClick = button.onClick, + colors = TangemButtonColors( + backgroundColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary, + contentColor = Dark6, + disabledBackgroundColor = TangemTheme.colors.button.disabled, + disabledContentColor = TangemTheme.colors.text.disabled, + ), + enabled = true, + showProgress = false, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + ) + } +} + +// region Preview +@Preview(widthDp = 360) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun OkxPromoNotification_Preview( + @PreviewParameter(OkxPromoNotificationPreviewProvider::class) data: NotificationConfig, +) { + TangemThemePreview { + OkxPromoNotification(data) + } +} + +private class OkxPromoNotificationPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + NotificationConfig( + title = resourceReference(R.string.swap_promo_title), + subtitle = resourceReference(R.string.swap_promo_text), + iconResId = R.drawable.img_okx_dex_logo, + onCloseClick = {}, + ), + NotificationConfig( + title = resourceReference(R.string.swap_promo_title), + subtitle = resourceReference(R.string.swap_promo_text), + iconResId = R.drawable.img_okx_dex_logo, + onCloseClick = {}, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.token_swap_promotion_button), + onClick = {}, + ), + ), + ) +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt index 451207be18..2a78787608 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt @@ -101,6 +101,7 @@ class TangemColors internal constructor( private set var attention by mutableStateOf(attention) private set + var constant by mutableStateOf(constant) fun update(other: Icon) { primary1 = other.primary1 @@ -111,6 +112,7 @@ class TangemColors internal constructor( accent = other.accent warning = other.warning attention = other.attention + constant = other.constant } } diff --git a/core/ui/src/main/res/drawable/img_okx_dex_logo.xml b/core/ui/src/main/res/drawable/img_okx_dex_logo.xml new file mode 100644 index 0000000000..3a4ba80759 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_okx_dex_logo.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index a92a4f5eb9..94a184fb73 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -32,8 +32,18 @@ internal class DefaultPromoRepository( }.getOrNull() } + override suspend fun getOkxPromoBanner(): PromoBanner? { + return runCatching(dispatchers.io) { + promoResponseConverter.convert( + tangemApi.getPromotionInfo(OKX) + .getOrThrow(), + ) + }.getOrNull() + } + private companion object { private const val CHANGELLY_NAME = "changelly" private const val TRAVALA = "travala" + private const val OKX = "okx" } } \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt index 80ba44157d..ce6d3fa236 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt @@ -1,8 +1,8 @@ package com.tangem.data.settings import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY -import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_TRAVALA_PROMO_SHOWN_KEY import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.store @@ -16,23 +16,23 @@ class DefaultPromoSettingsRepository( private val appPreferencesStore: AppPreferencesStore, ) : PromoSettingsRepository { override fun isReadyToShowWalletSwapPromo(): Flow { - return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY, true) + return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY, true) } override fun isReadyToShowTokenSwapPromo(): Flow { - return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY, true) + return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY, true) } override suspend fun setNeverToShowWalletSwapPromo() { appPreferencesStore.store( - key = IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY, + key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY, value = false, ) } override suspend fun setNeverToShowTokenSwapPromo() { appPreferencesStore.store( - key = IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY, + key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY, value = false, ) } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt index cc39985633..9e9156c366 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt @@ -1,18 +1,43 @@ package com.tangem.domain.tokens.models.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam sealed class TokenSwapPromoAnalyticsEvent( event: String, params: Map = mapOf(), -) : AnalyticsEvent("Swap Promo", event, params, null) { - - object Close : TokenSwapPromoAnalyticsEvent(event = "Button - Close") - - class Exchange( - token: String, +) : AnalyticsEvent(category = "Promotion", event = event, params = params) { + class NoticePromotionBanner( + source: AnalyticsParam.ScreensSources, + programName: ProgramName, ) : TokenSwapPromoAnalyticsEvent( - event = "Button - Exchange Now", - params = mapOf("Token" to token), + event = "Notice - Promotion Banner", + params = mapOf( + AnalyticsParam.SOURCE to source.value, + "Program Name" to programName.name, + ), ) + + class PromotionBannerClicked( + source: AnalyticsParam.ScreensSources, + programName: ProgramName, + action: BannerAction, + ) : TokenSwapPromoAnalyticsEvent( + event = "Promo Banner Clicked", + params = mapOf( + AnalyticsParam.SOURCE to source.value, + "Program Name" to programName.name, + "Action" to action.action, + ), + ) { + sealed class BannerAction(val action: String) { + data object Clicked : BannerAction(action = "Clicked") + data object Closed : BannerAction(action = "Closed") + } + } + + enum class ProgramName { + Travala, + OKX, + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index eb7296acea..93ff397011 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -84,7 +84,7 @@ class GetCurrencyWarningsUseCase( ): Flow { val currency = currencyStatus.currency val cryptoStatuses = operations.getCurrenciesStatusesSync() - val promoBanner = promoRepository.getChangellyPromoBanner() + val promoBanner = promoRepository.getOkxPromoBanner() return combine( showSwapPromoTokenUseCase().conflate(), flowOf(marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)).conflate(), diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt index ed42ab1cfc..51ba0a57f9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt @@ -7,4 +7,6 @@ interface PromoRepository { suspend fun getChangellyPromoBanner(): PromoBanner? suspend fun getTravalaPromoBanner(): PromoBanner? + + suspend fun getOkxPromoBanner(): PromoBanner? } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index a69442cd51..878dc66047 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -2,7 +2,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.analytics import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification @@ -35,12 +37,15 @@ internal class TokenDetailsNotificationsAnalyticsSender( -> TokenDetailsAnalyticsEvent.Notice.NotEnoughFee( currency = cryptoCurrency, ) + is TokenDetailsNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner( + programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX, + source = AnalyticsParam.ScreensSources.Token, + ) is TokenDetailsNotification.NetworksUnreachable, is TokenDetailsNotification.ExistentialDeposit, is TokenDetailsNotification.NetworksNoAccount, is TokenDetailsNotification.TopUpWithoutReserve, is TokenDetailsNotification.RentInfo, - is TokenDetailsNotification.SwapPromo, is TokenDetailsNotification.NetworkShutdown, is TokenDetailsNotification.HederaAssociateWarning, is TokenDetailsNotification.KoinosMana, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 950f24a218..a09065820b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -2,7 +2,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.componen import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.features.tokendetails.impl.R @@ -45,16 +48,9 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { val onCloseClick: () -> Unit, ) : TokenDetailsNotification( config = NotificationConfig( - title = resourceReference( - id = R.string.token_swap_changelly_promotion_title, - formatArgs = wrappedList("0%"), - ), - subtitle = resourceReference( - id = R.string.token_swap_changelly_promotion_message, - formatArgs = wrappedList("0%", startDateTime.dayOfMonth, endDateTime.dayOfMonth), - ), - iconResId = R.drawable.img_swap_promo, - backgroundResId = R.drawable.img_swap_promo_green_banner_background, + title = resourceReference(id = R.string.swap_promo_title), + subtitle = resourceReference(id = R.string.swap_promo_text), + iconResId = R.drawable.img_okx_dex_logo, onCloseClick = onCloseClick, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( text = resourceReference(id = com.tangem.core.ui.R.string.token_swap_promotion_button), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index d9eb9ec18d..c24ca651d8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -32,7 +32,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.notifications.NotificationWithBackground +import com.tangem.core.ui.components.notifications.OkxPromoNotification import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.core.ui.event.EventEffect @@ -111,7 +111,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { contentType = { it.config::class.java }, itemContent = { if (it is TokenDetailsNotification.SwapPromo) { - NotificationWithBackground( + OkxPromoNotification( config = it.config, modifier = itemModifier.animateItemPlacement(), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index d4e58af373..c2ff51d2d6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -5,6 +5,7 @@ import androidx.paging.cachedIn import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.deeplink.global.BuyCurrencyDeepLink import com.tangem.core.ui.clipboard.ClipboardManager @@ -709,14 +710,26 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onSwapPromoDismiss() { viewModelScope.launch(dispatchers.main) { shouldShowSwapPromoTokenUseCase.neverToShow() - analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Close) + analyticsEventsHandler.send( + TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( + source = AnalyticsParam.ScreensSources.Token, + programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX, + action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed, + ), + ) } } override fun onSwapPromoClick() { viewModelScope.launch(dispatchers.main) { shouldShowSwapPromoTokenUseCase.neverToShow() - analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Exchange(cryptoCurrency.symbol)) + analyticsEventsHandler.send( + TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( + source = AnalyticsParam.ScreensSources.Token, + programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX, + action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked, + ), + ) } onSwapClick(ScenarioUnavailabilityReason.None) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index f39a38511b..de6ddf8b1b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -121,38 +121,4 @@ sealed class WalletScreenAnalyticsEvent { data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped") } - - sealed class Promotion( - event: String, - params: Map = mapOf(), - ) : AnalyticsEvent(category = "Promotion", event = event, params = params) { - class NoticePromotionBanner( - source: AnalyticsParam.ScreensSources, - programName: String, - ) : Promotion( - event = "Notice - Promotion Banner", - params = mapOf( - AnalyticsParam.SOURCE to source.value, - "Program Name" to programName, - ), - ) - - class PromotionBannerClicked( - source: AnalyticsParam.ScreensSources, - programName: String, - action: BannerAction, - ) : Promotion( - event = "Promo Banner Clicked", - params = mapOf( - AnalyticsParam.SOURCE to source.value, - "Program Name" to programName, - "Action" to action.action, - ), - ) { - sealed class BannerAction(val action: String) { - data object Clicked : BannerAction(action = "Clicked") - data object Closed : BannerAction(action = "Closed") - } - } - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 59d4331387..398372e208 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -3,7 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState @@ -46,16 +46,19 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem is WalletNotification.Critical.BackupError -> MainScreen.BackupError - is WalletNotification.TravalaPromo -> WalletScreenAnalyticsEvent.Promotion.NoticePromotionBanner( + is WalletNotification.TravalaPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner( source = AnalyticsParam.ScreensSources.Main, - programName = "Travala", + programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala, + ) + is WalletNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner( + source = AnalyticsParam.ScreensSources.Main, + programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX, ) is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender] is WalletNotification.Informational.NoAccount, is WalletNotification.Warning.LowSignatures, is WalletNotification.Warning.SomeNetworksUnreachable, is WalletNotification.Warning.NetworksUnreachable, - is WalletNotification.SwapPromo, -> null } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index a2adf6b292..b0513c7656 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -6,7 +6,7 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.promo.PromoBanner import com.tangem.domain.settings.IsReadyToShowRateAppUseCase -import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase +import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency @@ -34,7 +34,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val getTokenListUseCase: GetTokenListUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, - private val shouldShowTravalaPromoWalletUseCase: ShouldShowTravalaPromoWalletUseCase, + private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, private val promoRepository: PromoRepository, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, private val backupValidator: BackupValidator, @@ -45,18 +45,18 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = userWallet.scanResponse.cardTypesResolver - val travalaPromoFlow = flow { emit(promoRepository.getTravalaPromoBanner()) } + val promoFlow = flow { emit(promoRepository.getOkxPromoBanner()) } return combine( flow = getTokenListUseCase.launch(userWallet.walletId).conflate(), flow2 = isReadyToShowRateAppUseCase().conflate(), flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), - flow4 = shouldShowTravalaPromoWalletUseCase().conflate(), - flow5 = travalaPromoFlow.conflate(), - ) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowTravalaPromo, promoBanner -> + flow4 = shouldShowSwapPromoWalletUseCase().conflate(), + flow5 = promoFlow.conflate(), + ) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner -> readyForRateAppNotification = true buildList { - addTravalaPromoNotification(shouldShowTravalaPromo, promoBanner, clickIntents) + addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents) addCriticalNotifications(userWallet) @@ -69,18 +69,16 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( } } - private fun MutableList.addTravalaPromoNotification( + private fun MutableList.addSwapPromoNotification( shouldShowPromo: Boolean, promoBanner: PromoBanner?, clickIntents: WalletClickIntents, ) { promoBanner ?: return - val promoNotification = WalletNotification.TravalaPromo( + val promoNotification = WalletNotification.SwapPromo( startDateTime = promoBanner.bannerState.timeline.start, endDateTime = promoBanner.bannerState.timeline.end, - bannerLink = promoBanner.bannerState.link, - onBookNowButtonClick = clickIntents::onTravalaPromoClick, - onCloseClick = clickIntents::onCloseTravalaPromoClick, + onCloseClick = clickIntents::onCloseSwapPromoClick, ) addIf( element = promoNotification, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index c34e986c29..4a3247b5e1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -2,7 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.pluralReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.feature.wallet.impl.R import org.joda.time.DateTime @@ -205,16 +208,9 @@ sealed class WalletNotification(val config: NotificationConfig) { val onCloseClick: () -> Unit, ) : WalletNotification( config = NotificationConfig( - title = resourceReference( - id = R.string.main_swap_changelly_promotion_title, - formatArgs = wrappedList("0%"), - ), - subtitle = resourceReference( - id = R.string.main_swap_changelly_promotion_message, - formatArgs = wrappedList("0%", startDateTime.dayOfMonth, endDateTime.dayOfMonth), - ), - iconResId = R.drawable.img_swap_promo, - backgroundResId = R.drawable.img_swap_promo_green_banner_background, + title = resourceReference(id = R.string.swap_promo_title), + subtitle = resourceReference(id = R.string.swap_promo_text), + iconResId = R.drawable.img_okx_dex_logo, onCloseClick = onCloseClick, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 6b783e0eb0..08a341ebb7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.notifications.NotificationWithBackground +import com.tangem.core.ui.components.notifications.OkxPromoNotification import com.tangem.core.ui.components.notifications.TravalaNotificationWithBackground import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification @@ -29,7 +29,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList { - NotificationWithBackground( + OkxPromoNotification( config = it.config, modifier = modifier.animateItemPlacement(), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 21982d8010..d2af11e81b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -14,13 +14,13 @@ import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.models.UnlockWalletsError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler @@ -214,6 +214,13 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } override fun onCloseSwapPromoClick() { + analyticsEventHandler.send( + TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( + source = AnalyticsParam.ScreensSources.Main, + programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX, + action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed, + ), + ) viewModelScope.launch(dispatchers.main) { shouldShowSwapPromoWalletUseCase.neverToShow() } @@ -221,10 +228,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onTravalaPromoClick(link: String?) { analyticsEventHandler.send( - WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked( + TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, - programName = "Travala", - action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Clicked, + programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala, + action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked, ), ) link?.let { @@ -236,10 +243,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onCloseTravalaPromoClick() { analyticsEventHandler.send( - WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked( + TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, - programName = "Travala", - action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Closed, + programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala, + action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed, ), ) viewModelScope.launch(dispatchers.main) { From 7789f437fa604fcad72573eb65e22b1f7268ffe5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 23:40:06 +0800 Subject: [PATCH 20/48] Updated on 2026-08-14 --- .../presentation/wallet/state/WalletStateController.kt | 2 ++ .../presentation/wallet/viewmodels/WalletViewModel.kt | 8 ++++++++ gradle/dependencies.toml | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index e8c3c9114c..895c894ab0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -17,6 +17,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -41,6 +42,7 @@ internal class WalletStateController @Inject constructor( } fun update(transformer: WalletScreenStateTransformer) { + Timber.d("Applying: ${transformer::class.simpleName}") mutableUiState.update(function = transformer::transform) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index dbad1dde46..058a853a52 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -216,6 +216,14 @@ internal class WalletViewModel @Inject constructor( is WalletsUpdateActionResolver.Action.DeleteWallet -> deleteWallet(action) is WalletsUpdateActionResolver.Action.UnlockWallet -> unlockWallet(action) is WalletsUpdateActionResolver.Action.UpdateWalletCardCount -> { + // refresh loader to use actual user wallet + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + clickIntents = clickIntents, + isRefresh = true, + coroutineScope = viewModelScope, + ) + stateHolder.update(transformer = UpdateWalletCardsCountTransformer(action.selectedWallet)) } is WalletsUpdateActionResolver.Action.UpdateWalletName -> { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 88ac8d88ab..5fae08e5f1 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -87,7 +87,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.12-691" +tangemBlockchainSdk = "release-app_5.12-694" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.12-367" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From c4af41a48a676facc3a53c83894649456fcce96a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Jun 2024 20:31:58 +0400 Subject: [PATCH 21/48] Updated on 2026-08-14 --- .../BiometricUserWalletsListManager.kt | 63 ++++++++----------- .../data/card/sdk/DefaultCardSdkProvider.kt | 2 +- gradle/dependencies.toml | 4 +- 3 files changed, 28 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index 52e9be2c6d..199b92bf71 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -152,19 +152,29 @@ internal class BiometricUserWalletsListManager( return CompletionResult.Success(Unit) } - changeSelectedUserWalletIdIfNeeded(idsToRemove) + if (idsToRemove.size == state.value.userWallets.size) { + return clear() + } return sensitiveInformationRepository.delete(idsToRemove) .flatMap { publicInformationRepository.delete(idsToRemove) } .map { keysRepository.delete(idsToRemove) } .map { state.update { prevState -> - val newUserWallets = prevState.userWallets.filter { it.walletId !in idsToRemove } + val remainingWallets = prevState.userWallets.filter { it.walletId !in idsToRemove } + + val isSelectedWalletDeleted = prevState.selectedUserWalletId in idsToRemove + val newSelectedUserWallet = findOrSetSelectedWallet( + prevSelectedWalletId = prevState.selectedUserWalletId, + userWallets = remainingWallets, + ignorePrevSelectedWallet = isSelectedWalletDeleted, + ) prevState.copy( encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in idsToRemove }, - userWallets = newUserWallets, - isLocked = newUserWallets.any { it.isLocked }, + userWallets = remainingWallets, + isLocked = remainingWallets.any { it.isLocked }, + selectedUserWalletId = newSelectedUserWallet?.walletId, ) } } @@ -173,10 +183,10 @@ internal class BiometricUserWalletsListManager( override suspend fun clear(): CompletionResult { return sensitiveInformationRepository.clear() .flatMap { publicInformationRepository.clear() } - .map { keysRepository.clear() } .map { + keysRepository.clear() selectedUserWalletRepository.set(null) - lock() + state.value = State() } } @@ -301,47 +311,24 @@ internal class BiometricUserWalletsListManager( private fun findOrSetSelectedWallet( prevSelectedWalletId: UserWalletId?, userWallets: List, + ignorePrevSelectedWallet: Boolean = false, ): UserWallet? { - val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get() - var possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId) + var possibleSelectedUserWallet: UserWallet? = null + + if (!ignorePrevSelectedWallet) { + val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get() + possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId) + } if (possibleSelectedUserWallet == null || possibleSelectedUserWallet.isLocked) { possibleSelectedUserWallet = userWallets.firstOrNull { !it.isLocked } ?: userWallets.firstOrNull() - - if (possibleSelectedUserWallet != null) { - selectedUserWalletRepository.set(possibleSelectedUserWallet.walletId) - } } + selectedUserWalletRepository.set(possibleSelectedUserWallet?.walletId) + return possibleSelectedUserWallet } - private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List) { - val remainingWallets = state.value.userWallets.filter { - it.walletId !in walletsIdsToRemove - } - val selectedWallet = findSelectedUserWallet() - when { - remainingWallets.isEmpty() -> { - state.update { prevState -> - prevState.copy( - selectedUserWalletId = null, - ) - } - selectedUserWalletRepository.set(null) - } - !remainingWallets.contains(selectedWallet) -> { - val newSelectedWallet = remainingWallets.firstOrNull { !it.isLocked } - state.update { prevState -> - prevState.copy( - selectedUserWalletId = newSelectedWallet?.walletId, - ) - } - selectedUserWalletRepository.set(newSelectedWallet?.walletId) - } - } - } - private fun findSelectedUserWallet( userWallets: List = state.value.userWallets, selectedUserWalletId: UserWalletId? = state.value.selectedUserWalletId, diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt index d8427a907f..8c91326d64 100644 --- a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt +++ b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt @@ -40,7 +40,7 @@ internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, C linkedTerminal = true, allowUntrustedCards = true, filter = CardFilter( - allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(), + allowedCardTypes = FirmwareVersion.FirmwareType.entries.toList(), maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33), batchIdFilter = CardFilter.Companion.ItemFilter.Deny( items = setOf("0027", "0030", "0031", "0035"), diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 5fae08e5f1..d0f16ceba2 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -89,8 +89,8 @@ markdown = "0.7.2" # region Tangem tangemBlockchainSdk = "release-app_5.12-694" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.12-367" -#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ +#tangemCardSdk = "release-app_5.12-367" +tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem # region Tools From 1c6a1733620b22bc52e69c027b117390e5ab7e55 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jul 2024 16:31:14 +0400 Subject: [PATCH 22/48] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 13 +-- .../java/com/tangem/tap/TangemApplication.kt | 1 + .../ui/components/WelcomeScreenContent.kt | 2 +- .../com/tangem/data/card/di/CardSdkModule.kt | 4 +- .../data/card/sdk/CardSdkLifecycleObserver.kt | 17 ---- .../com/tangem/data/card/sdk/CardSdkOwner.kt | 13 +++ .../data/card/sdk/DefaultCardSdkProvider.kt | 96 ++++++++++++++++--- 7 files changed, 104 insertions(+), 42 deletions(-) delete mode 100644 data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt create mode 100644 data/card/src/main/java/com/tangem/data/card/sdk/CardSdkOwner.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 982967b3ea..6753d90394 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -37,7 +37,7 @@ import com.tangem.core.navigation.email.EmailSender import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.data.card.sdk.CardSdkLifecycleObserver +import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository @@ -108,7 +108,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lateinit var testerRouter: TesterRouter @Inject - lateinit var cardSdkLifecycleObserver: CardSdkLifecycleObserver + lateinit var cardSdkOwner: CardSdkOwner @Inject lateinit var cardSdkConfigRepository: CardSdkConfigRepository @@ -218,7 +218,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private fun installActivityDependencies() { store.dispatch(NavigationAction.ActivityCreated(WeakReference(this))) - cardSdkLifecycleObserver.onCreate(context = this) + cardSdkOwner.register(activity = this) tangemSdkManager = injectedTangemSdkManager appStateHolder.tangemSdkManager = tangemSdkManager backupService = BackupService.init(cardSdkConfigRepository.sdk, this) @@ -327,7 +327,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onDestroy() { store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this))) intentProcessor.removeAll() - cardSdkLifecycleObserver.onDestroy(this) super.onDestroy() } @@ -363,12 +362,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - /* - * FIXME: Test functionality. TangemSdk is null on some devices when HomeAction.Read is called - * inside IntentHandler. - */ - cardSdkLifecycleObserver.onCreate(context = this) - lifecycleScope.launch { intentProcessor.handleIntent(intent, true) } diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 73e4bcd76f..d9b90a7bb9 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -353,6 +353,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { Log.Level.Network, Log.Level.Error, Log.Level.Biometric, + Log.Level.Info, ) return TangemLogCollector(logLevels, LogFormat.StairsFormatter()) } diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt index 5aca214e93..7fb08b3681 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt @@ -13,8 +13,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.wallet.R @Suppress("LongMethod") diff --git a/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt b/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt index c88f2dbfc2..891c418cb1 100644 --- a/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt +++ b/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt @@ -1,6 +1,6 @@ package com.tangem.data.card.di -import com.tangem.data.card.sdk.CardSdkLifecycleObserver +import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.data.card.sdk.DefaultCardSdkProvider import dagger.Binds @@ -19,5 +19,5 @@ internal interface CardSdkModule { @Binds @Singleton - fun providerCardSdkLifecycleObserver(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkLifecycleObserver + fun providerCardSdkLifecycleObserver(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkOwner } \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt deleted file mode 100644 index 212ff3a8bf..0000000000 --- a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.data.card.sdk - -import android.content.Context - -/** - * Lifecycle observer for creating Card SDK instance - * -[REDACTED_AUTHOR] - */ -interface CardSdkLifecycleObserver { - - /** Callback of creating activity [context] */ - fun onCreate(context: Context) - - /** Callback of destroying activity [context] */ - fun onDestroy(context: Context) -} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkOwner.kt b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkOwner.kt new file mode 100644 index 0000000000..a5654ef6ec --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkOwner.kt @@ -0,0 +1,13 @@ +package com.tangem.data.card.sdk + +import androidx.fragment.app.FragmentActivity + +/** + * Lifecycle observer for creating Card SDK instance + * +[REDACTED_AUTHOR] + */ +interface CardSdkOwner { + + fun register(activity: FragmentActivity) +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt index 8c91326d64..cdc3c2e305 100644 --- a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt +++ b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt @@ -1,12 +1,21 @@ package com.tangem.data.card.sdk -import android.content.Context import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.tangem.Log import com.tangem.TangemSdk import com.tangem.common.CardFilter +import com.tangem.common.authentication.AuthenticationManager import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.Config -import com.tangem.sdk.extensions.initWithBiometrics +import com.tangem.common.services.secure.SecureStorage +import com.tangem.crypto.bip39.Wordlist +import com.tangem.sdk.DefaultSessionViewDelegate +import com.tangem.sdk.extensions.* +import com.tangem.sdk.nfc.NfcManager +import com.tangem.sdk.storage.create import javax.inject.Inject import javax.inject.Singleton @@ -16,24 +25,87 @@ import javax.inject.Singleton [REDACTED_AUTHOR] */ @Singleton -internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, CardSdkLifecycleObserver { +internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, CardSdkOwner { override val sdk: TangemSdk - get() = requireNotNull(value = _sdk) { "Impossible to get the TangemSdk when activity is destroyed" } + get() = requireNotNull(value = holder?.sdk) { + "Impossible to get the TangemSdk when activity is destroyed" + } - private var _sdk: TangemSdk? = null + private val observer: LifecycleObserver = Observer() - override fun onCreate(context: Context) { - _sdk = TangemSdk.initWithBiometrics(activity = context as FragmentActivity, config = config) + private var holder: Holder? = null + + override fun register(activity: FragmentActivity) { + Log.info { "Tangem SDK owner registered" } + + if (holder != null) { + unsubscribeAndCleanup() + } + + initialize(activity) + + activity.lifecycle.addObserver(observer) } - override fun onDestroy(context: Context) { - // Commented out to prevent crash on getting sdk when it's null. - // FIXME: We still should find the real cause and fix it properly. - // idea: pass everywhere DefaultCardSdkProvider instead sdk to reach lazy access to sdk property - // _sdk = null + private fun initialize(activity: FragmentActivity) { + val secureStorage = SecureStorage.create(activity) + val nfcManager = TangemSdk.initNfcManager(activity) + val authenticationManager = TangemSdk.initAuthenticationManager(activity) + val keystoreManager = TangemSdk.initKeystoreManager(authenticationManager, secureStorage) + + val viewDelegate = DefaultSessionViewDelegate(nfcManager, activity) + viewDelegate.sdkConfig = config + + val sdk = TangemSdk( + reader = nfcManager.reader, + viewDelegate = viewDelegate, + secureStorage = secureStorage, + authenticationManager = authenticationManager, + keystoreManager = keystoreManager, + wordlist = Wordlist.getWordlist(activity), + config = config, + ) + + holder = Holder( + activity = activity, + nfcManager = nfcManager, + authenticationManager = authenticationManager, + sdk = sdk, + ) + + Log.info { "Tangem SDK initialized" } } + private fun unsubscribeAndCleanup() { + with(receiver = holder ?: return) { + nfcManager.unsubscribe(activity) + authenticationManager.unsubscribe(activity) + + activity.lifecycle.removeObserver(observer) + } + + holder = null + + Log.info { "Tangem SDK unsubscribed and cleaned up" } + } + + inner class Observer : DefaultLifecycleObserver { + + override fun onDestroy(owner: LifecycleOwner) { + Log.info { "Tangem SDK owner destroyed" } + + unsubscribeAndCleanup() + } + } + + data class Holder( + val activity: FragmentActivity, + val sdk: TangemSdk, + val nfcManager: NfcManager, + val authenticationManager: AuthenticationManager, + ) + private companion object { val config = Config( From a5c2f2d6acd05b9bb183d75e0d8fe4de169939af Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jul 2024 19:13:39 +0400 Subject: [PATCH 23/48] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 1 + .../main/java/com/tangem/tap/MainActivity.kt | 27 ++++++++++--------- .../tangem/core/ui/screen/ComposeFragment.kt | 13 +++++++++ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d52735ce7..536e770195 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,6 +55,7 @@ + private lateinit var appThemeModeFlow: SharedFlow // TODO: fixme: inject through DI private val intentProcessor: IntentProcessor = IntentProcessor() @@ -172,10 +173,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private val onActivityResultCallbacks = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { - val splashScreen = installSplashScreen() - installAppTheme() // We need to call it before onCreate to prevent unnecessary activity recreation + val splashScreen = installSplashScreen() + super.onCreate(savedInstanceState) splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown } @@ -248,14 +249,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private fun installAppTheme() { appThemeModeFlow = createAppThemeModeFlow() - val mode = runBlocking { appThemeModeFlow.filterNotNull().first() } + val mode = runBlocking { appThemeModeFlow.first() } updateAppTheme(mode) } private fun observeAppThemeModeUpdates() { appThemeModeFlow - .filterNotNull() .flowWithLifecycle(lifecycle) .onEach(::updateAppTheme) .launchIn(lifecycleScope) @@ -273,15 +273,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } - private fun createAppThemeModeFlow(): SharedFlow { + private fun createAppThemeModeFlow(): SharedFlow { val tangemApplication = application as TangemApplication return tangemApplication.getAppThemeModeUseCase() + .filterNotNull() .map { maybeMode -> maybeMode.getOrElse { AppThemeMode.DEFAULT } } + .distinctUntilChanged() .shareIn( - scope = lifecycleScope + Dispatchers.IO, + scope = lifecycleScope, started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000), ) } @@ -337,17 +339,18 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun updateAppTheme(appThemeMode: AppThemeMode) { - MutableAppThemeModeHolder.value = appThemeMode - MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme() - val mode = when (appThemeMode) { AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES AppThemeMode.FORCE_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO AppThemeMode.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM } - setDefaultNightMode(mode) - delegate.localNightMode = mode + if (mode != getDefaultNightMode()) { + setDefaultNightMode(mode) + } + + MutableAppThemeModeHolder.value = appThemeMode + MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme() } private fun isDarkTheme(): Boolean { diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt index fb811871af..c60bf1ba7b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.screen +import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -22,6 +23,18 @@ abstract class ComposeFragment : Fragment(), ComposeScreen { } } + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + /* + * We need to manually dispatch configuration changes to the Compose view. + * + + * `android:configChanges="uiMode"` is set in the manifest. + * */ + view?.dispatchConfigurationChanged(newConfig) + } + /** * Inflates transitions for the fragment. Override this method to customize * enter and exit transitions for the fragment. From 8e136078264357deba3d391c875f2f13e9562098 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Jul 2024 12:39:38 +0400 Subject: [PATCH 24/48] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/MainActivity.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 874d03de60..4b65cf26cf 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -446,15 +446,16 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { - val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0 + val backStack = store.state.navigationState.backStack + val isOnInitialScreen = backStack.all { it == AppScreen.Welcome || it == AppScreen.Home } val isNotScannedBefore = store.state.globalState.scanResponse == null val isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted when { - !backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> { + !isOnInitialScreen && isNotScannedBefore && isOnboardingServiceNotActive -> { navigateToInitialScreen(intentWhichStartedActivity) } - backStackIsEmpty -> { + backStack.isEmpty() -> { navigateToInitialScreen(intentWhichStartedActivity) } else -> Unit From 68941667118b069a1d2e6ff9e966ddf3d12d9601 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 17:12:10 +0400 Subject: [PATCH 25/48] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 4b65cf26cf..fcd268478a 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -16,8 +16,9 @@ import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate -import androidx.appcompat.app.AppCompatDelegate.getDefaultNightMode import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode +import androidx.compose.ui.graphics.toArgb +import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.os.bundleOf @@ -38,6 +39,7 @@ import com.tangem.core.navigation.email.EmailSender import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase @@ -172,8 +174,22 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private val onActivityResultCallbacks = mutableListOf() + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + /* + * We need to manually change the background color of the activity when the UI mode changes to prevent + * flickering when navigating between fragments. + * + + * `android:configChanges="uiMode"` is set in the manifest. + * */ + updateAppBackground() + } + override fun onCreate(savedInstanceState: Bundle?) { - installAppTheme() // We need to call it before onCreate to prevent unnecessary activity recreation + // We need to call it before onCreate to prevent unnecessary activity recreation + installAppTheme() val splashScreen = installSplashScreen() @@ -345,14 +361,22 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac AppThemeMode.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM } - if (mode != getDefaultNightMode()) { - setDefaultNightMode(mode) - } + setDefaultNightMode(mode) MutableAppThemeModeHolder.value = appThemeMode MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme() } + private fun updateAppBackground() { + val backgroundColor = if (isDarkTheme()) { + TangemColorPalette.Dark6 + } else { + TangemColorPalette.White + } + + findViewById(R.id.fragment_container).setBackgroundColor(backgroundColor.toArgb()) + } + private fun isDarkTheme(): Boolean { return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI_MODE_NIGHT_YES -> true From 01ba6b5ac778687be9b0e846531eb070829561e8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Jul 2024 15:15:08 +0400 Subject: [PATCH 26/48] Updated on 2026-08-14 --- gradle/dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index d0f16ceba2..a821c9080b 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -89,8 +89,8 @@ markdown = "0.7.2" # region Tangem tangemBlockchainSdk = "release-app_5.12-694" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -#tangemCardSdk = "release-app_5.12-367" -tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ +tangemCardSdk = "release-app_5.12-369" +#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem # region Tools From 47765df107d4c92065eebc6c89d4934fbf2135d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Jul 2024 16:59:58 +0400 Subject: [PATCH 27/48] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 28 +++++++++---------- .../tangem/tap/di/data/CardSDKLoggerModule.kt | 1 + 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index fcd268478a..ea644f792a 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -174,19 +174,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private val onActivityResultCallbacks = mutableListOf() - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - - /* - * We need to manually change the background color of the activity when the UI mode changes to prevent - * flickering when navigating between fragments. - * - - * `android:configChanges="uiMode"` is set in the manifest. - * */ - updateAppBackground() - } - override fun onCreate(savedInstanceState: Bundle?) { // We need to call it before onCreate to prevent unnecessary activity recreation installAppTheme() @@ -294,10 +281,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac return tangemApplication.getAppThemeModeUseCase() .filterNotNull() + .distinctUntilChanged() .map { maybeMode -> maybeMode.getOrElse { AppThemeMode.DEFAULT } } - .distinctUntilChanged() .shareIn( scope = lifecycleScope, started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000), @@ -367,6 +354,19 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme() } + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + /* + * We need to manually change the background color of the activity when the UI mode changes to prevent + * flickering when navigating between fragments. + * + + * `android:configChanges="uiMode"` is set in the manifest. + * */ + updateAppBackground() + } + private fun updateAppBackground() { val backgroundColor = if (isDarkTheme()) { TangemColorPalette.Dark6 diff --git a/app/src/main/java/com/tangem/tap/di/data/CardSDKLoggerModule.kt b/app/src/main/java/com/tangem/tap/di/data/CardSDKLoggerModule.kt index 6da93efeb4..9b326b64c7 100644 --- a/app/src/main/java/com/tangem/tap/di/data/CardSDKLoggerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/CardSDKLoggerModule.kt @@ -33,6 +33,7 @@ internal object CardSDKLoggerModule { Log.Level.Network, Log.Level.Error, Log.Level.Biometric, + Log.Level.Info, ) return TangemCardSDKLogger( From 999e4debb71d665bb1dbb879e075f9f47c9b9e5c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Jul 2024 21:45:43 +0100 Subject: [PATCH 28/48] Updated on 2026-08-14 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7218026460..b2ef2745d2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs = -Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs = -Xmx6144m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects From d992cc5c9840caeaf3038c3426c1c8c3fbe64f20 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Jul 2024 13:11:18 +0100 Subject: [PATCH 29/48] Updated on 2026-08-14 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7218026460..b2ef2745d2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs = -Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs = -Xmx6144m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects From a2425acf096e48525b4bb668f77040355f53ae9b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Jul 2024 18:05:26 +0800 Subject: [PATCH 30/48] Updated on 2026-08-14 --- .../viewmodels/AddCustomTokenViewModel.kt | 11 ++++++++--- gradle/dependencies.toml | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 34944cf67a..d725069217 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -370,6 +370,8 @@ internal class AddCustomTokenViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { val tokenAddress = convertTokenAddress(selectedNetwork, address) + ?: error("TokenAddress is invalid") + featureInteractor.findToken(address = tokenAddress, blockchain = selectedNetwork) } .onSuccess { token -> @@ -602,7 +604,8 @@ internal class AddCustomTokenViewModel @Inject constructor( val contractAddress = convertTokenAddress( blockchain = networkSelectorValue, address = uiState.form.contractAddressInputField.value, - ) + ) ?: return false // invalid address can't be "already added" + return currentCryptoCurrencies .filterIsInstance() .any { token -> @@ -859,7 +862,8 @@ internal class AddCustomTokenViewModel @Inject constructor( val contractAddress = convertTokenAddress( blockchain = blockchain, address = foundToken?.network?.contractAddress ?: uiState.form.contractAddressInputField.value, - ) + ) ?: error("Contract address is invalid") // impossible to add a token with invalid address + CustomCurrency.CustomToken( token = Token( name = uiState.form.tokenNameInputField.value, @@ -897,7 +901,8 @@ internal class AddCustomTokenViewModel @Inject constructor( } } - private fun convertTokenAddress(blockchain: Blockchain, address: String): String { + /** Convert [address] to single address for specific [blockchain] or return null if invalid */ + private fun convertTokenAddress(blockchain: Blockchain, address: String): String? { return when (blockchain) { Blockchain.Hedera, Blockchain.HederaTestnet -> hederaAddressConverter.convertToTokenId(address) Blockchain.Cardano -> { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index a821c9080b..3123d85dbb 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -87,7 +87,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.12-694" +tangemBlockchainSdk = "release-app_5.12-697" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.12-369" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 7c3d6bf9af4118afb3b4f54972e11cae9cfb59f4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Jul 2024 22:40:09 +0800 Subject: [PATCH 31/48] Updated on 2026-08-14 --- .../models/response/ExchangeProvider.kt | 3 ++ core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + .../swap/converters/SwapPairInfoConverter.kt | 2 ++ .../domain/models/domain/SwapPairLeast.kt | 2 ++ .../swap/models/states/ProviderState.kt | 7 ++-- .../tangem/feature/swap/ui/ProviderItem.kt | 32 +++++++++++++------ .../tangem/feature/swap/ui/StateBuilder.kt | 12 +++++-- 8 files changed, 46 insertions(+), 14 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt index 8b224858c1..bda2f0e17f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt @@ -23,6 +23,9 @@ data class ExchangeProvider( @Json(name = "privacyPolicy") val privacyPolicy: String?, + + @Json(name = "recommended") + val isRecommended: Boolean = false, ) enum class ExchangeProviderType { diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 453bf3dd9b..e70467d63f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -239,6 +239,7 @@ Доступно с %s Недоступно для этой пары Требуется разрешение + Рекомендовано Условиями использования Токены не найдены. Пожалуйста, попробуйте другой запрос ID: %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index c712fdab72..40b0b45e01 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -238,6 +238,7 @@ Available from %s Unavailable for this pair Permission Required + Recommended Terms of Use No tokens found. Please try another request ID: %s diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index cdaa277dac..ec5a88e56e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -45,6 +45,7 @@ class SwapPairInfoConverter : Converter BestTradeItem( - Modifier.padding(start = TangemTheme.dimens.spacing4), - ) - ProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem( - Modifier.padding(start = TangemTheme.dimens.spacing4), - ) - ProviderState.AdditionalBadge.Empty -> { - // no-op - } + ProviderState.AdditionalBadge.BestTrade -> BestTradeItem(badgeModifier) + ProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem(badgeModifier) + ProviderState.AdditionalBadge.Recommended -> RecommendedItem(badgeModifier) + ProviderState.AdditionalBadge.Empty -> Unit } } Row( @@ -385,6 +382,23 @@ private fun PermissionBadgeItem(modifier: Modifier = Modifier) { } } +@Composable +private fun RecommendedItem(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f), + shape = TangemTheme.shapes.roundedCornersLarge, + ), + ) { + Text( + text = stringResource(id = R.string.express_provider_recommended), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.icon.accent, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), + ) + } +} + // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 880518dcdb..d4e4617075 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1541,7 +1541,9 @@ internal class StateBuilder( val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" - val badge = if (isNeedBestRateBadge && isBestRate) { + val badge = if (isRecommended) { + ProviderState.AdditionalBadge.Recommended + } else if (isNeedBestRateBadge && isBestRate) { ProviderState.AdditionalBadge.BestTrade } else { ProviderState.AdditionalBadge.Empty @@ -1555,7 +1557,11 @@ internal class StateBuilder( additionalBadge = badge, selectionType = selectionType, percentLowerThenBest = PercentDifference.Empty, - namePrefix = ProviderState.PrefixType.PROVIDED_BY, + namePrefix = if (badge !is ProviderState.AdditionalBadge.Recommended) { + ProviderState.PrefixType.PROVIDED_BY + } else { + ProviderState.PrefixType.NONE + }, onProviderClick = onProviderClick, ) } @@ -1570,6 +1576,8 @@ internal class StateBuilder( val rateString = toTokenInfo.tokenAmount.getFormattedCryptoAmount(toTokenInfo.cryptoCurrencyStatus.currency) val additionalBadge = if (state.permissionState is PermissionDataState.PermissionReadyForRequest) { ProviderState.AdditionalBadge.PermissionRequired + } else if (isRecommended) { + ProviderState.AdditionalBadge.Recommended } else { ProviderState.AdditionalBadge.Empty } From ea8ed2812d856f0dcf405b86bff0659896fa26b3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Jul 2024 19:35:52 +0800 Subject: [PATCH 32/48] Updated on 2026-08-14 --- .../feature/swap/models/states/ProviderState.kt | 1 + .../java/com/tangem/feature/swap/ui/StateBuilder.kt | 12 ++---------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index 6fde672bc0..5fb9348584 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -55,6 +55,7 @@ sealed class ProviderState { NONE, CLICK, SELECT } + // Prefix will be disabled in 5.12 but mechanism is still implemented @Immutable enum class PrefixType { NONE, PROVIDED_BY diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index d4e4617075..da536bd046 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1557,11 +1557,7 @@ internal class StateBuilder( additionalBadge = badge, selectionType = selectionType, percentLowerThenBest = PercentDifference.Empty, - namePrefix = if (badge !is ProviderState.AdditionalBadge.Recommended) { - ProviderState.PrefixType.PROVIDED_BY - } else { - ProviderState.PrefixType.NONE - }, + namePrefix = ProviderState.PrefixType.NONE, onProviderClick = onProviderClick, ) } @@ -1627,11 +1623,7 @@ internal class StateBuilder( subtitle = alertText, additionalBadge = ProviderState.AdditionalBadge.Empty, percentLowerThenBest = PercentDifference.Empty, - namePrefix = if (selectionType != ProviderState.SelectionType.SELECT) { - ProviderState.PrefixType.PROVIDED_BY - } else { - ProviderState.PrefixType.NONE - }, + namePrefix = ProviderState.PrefixType.NONE, onProviderClick = onProviderClick, ) } From c68766a67673270f86bde42545cab3d061abc0c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Jul 2024 14:10:57 +0100 Subject: [PATCH 33/48] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 12 +++++ .../repository/DefaultQuotesRepository.kt | 13 +++++ ...RefreshMultiCurrencyWalletQuotesUseCase.kt | 53 +++++++++++++++++++ .../tangem/domain/tokens/error/QuotesError.kt | 5 ++ .../tokens/repository/QuotesRepository.kt | 2 + .../tokens/repository/MockQuotesRepository.kt | 2 + .../wallet/viewmodels/WalletViewModel.kt | 18 ++++--- 7 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/QuotesError.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 40ba3c5084..5990835735 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -370,4 +370,16 @@ internal object TokensDomainModule { networksRepository = networksRepository, ) } + + @Provides + @Singleton + fun provideRefreshMultiCurrencyWalletQuotesUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + ): RefreshMultiCurrencyWalletQuotesUseCase { + return RefreshMultiCurrencyWalletQuotesUseCase( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + ) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index 1dee0062ce..f6c7ade8b4 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -51,6 +51,19 @@ internal class DefaultQuotesRepository( .flowOn(dispatchers.io) } + override suspend fun fetchQuotes(currenciesIds: Set) { + withContext(dispatchers.io) { + val selectedAppCurrency = requireNotNull( + value = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, + ), + lazyMessage = { "Unable to get selected application currency to update quotes" }, + ) + + fetchExpiredQuotes(currenciesIds, selectedAppCurrency.id, true) + } + } + override suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set { return withContext(dispatchers.io) { val selectedAppCurrency = requireNotNull( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt new file mode 100644 index 0000000000..e108b21819 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt @@ -0,0 +1,53 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.tokens.error.QuotesError +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope + +class RefreshMultiCurrencyWalletQuotesUseCase( + private val quotesRepository: QuotesRepository, + private val currenciesRepository: CurrenciesRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId): Either { + return either { + val currencies = fetchCurrencies(userWalletId = userWalletId) + .getOrElse { raise(QuotesError.DataError(it)) } + + coroutineScope { + val fetchQuotes = async { + fetchQuotes( + currenciesIds = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::id), + ) + } + + awaitAll(fetchQuotes) + } + } + } + + private suspend fun fetchCurrencies(userWalletId: UserWalletId): Either> { + return either { + catch( + block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, false) }, + catch = { raise(it) }, + ) + } + } + + private suspend fun fetchQuotes(currenciesIds: Set) { + catch( + block = { quotesRepository.fetchQuotes(currenciesIds) }, + catch = { /* Ignore error */ }, + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/QuotesError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/QuotesError.kt new file mode 100644 index 0000000000..f5601eb6db --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/QuotesError.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.tokens.error + +sealed class QuotesError { + data class DataError(val cause: Throwable) : QuotesError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index 64a0d29a12..4ebd284421 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -31,4 +31,6 @@ interface QuotesRepository { suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? + + suspend fun fetchQuotes(currenciesIds: Set) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt index ae960f191a..3d45f98879 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -25,4 +25,6 @@ internal class MockQuotesRepository( return quotes.map { it.getOrElse { e -> throw e } }.first() .first { it.rawCurrencyId == currencyId.rawCurrencyId } } + + override suspend fun fetchQuotes(currenciesIds: Set) {} } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 058a853a52..713453127a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -4,11 +4,13 @@ import androidx.compose.runtime.MutableState import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase +import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase @@ -21,7 +23,6 @@ import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContent import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletPullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender @@ -61,6 +62,7 @@ internal class WalletViewModel @Inject constructor( private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender, private val walletDeepLinksHandler: WalletDeepLinksHandler, private val walletNameMigrationUseCase: WalletNameMigrationUseCase, + private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase, ) : ViewModel() { val uiState: StateFlow = stateHolder.uiState @@ -178,7 +180,7 @@ internal class WalletViewModel @Inject constructor( } } - // We need to update the current wallet if the application was in the background for more than 10 seconds + // We need to update the current wallet quotes if the application was in the background for more than 10 seconds // and then returned to the foreground private fun subscribeToScreenBackgroundState() { screenLifecycleProvider.isBackgroundState @@ -186,7 +188,7 @@ internal class WalletViewModel @Inject constructor( refreshWalletJobHolder.cancel() when { isBackground -> needToRefreshTimer() - needToRefreshWallet && !isBackground -> triggerRefreshWallet() + needToRefreshWallet && !isBackground -> triggerRefreshWalletQuotes() } } .launchIn(viewModelScope) @@ -199,13 +201,15 @@ internal class WalletViewModel @Inject constructor( }.saveIn(refreshWalletJobHolder) } - private fun triggerRefreshWallet() { + private fun triggerRefreshWalletQuotes() { needToRefreshWallet = false val state = stateHolder.uiState.value val wallet = state.wallets.getOrNull(state.selectedWalletIndex) ?: return - wallet.pullToRefreshConfig.onRefresh.invoke( - WalletPullToRefreshConfig.ShowRefreshState(false), - ) + viewModelScope.launch { + refreshMultiCurrencyWalletQuotesUseCase(wallet.walletCardState.id).getOrElse { + Timber.e("Failed to refreshMultiCurrencyWalletQuotesUseCase $it") + } + }.saveIn(refreshWalletJobHolder) } private suspend fun updateWallets(action: WalletsUpdateActionResolver.Action) { From 785a7e7b5986034934daf50b6e6543264900a379 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Jul 2024 16:16:09 +0100 Subject: [PATCH 34/48] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 3123d85dbb..e73b861d62 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -87,7 +87,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.12-697" +tangemBlockchainSdk = "release-app_5.12-698" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.12-369" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From a363d4d4760126b0d5d442b265a0f0e67a82d02b Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 7 Jul 2024 19:05:03 +0100 Subject: [PATCH 35/48] Updated on 2026-08-14 --- .../domain/models/domain/SavedSwapTransactionListModel.kt | 1 + .../feature/swap/domain/models/domain/SwapPairLeast.kt | 5 ++++- .../com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt index 3eb907948a..d771aa4ecb 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt @@ -26,6 +26,7 @@ data class SavedSwapTransactionListModelInner( val transactions: List, ) +// TODO refactor to use separate models to store data class SavedSwapTransactionModel( val txId: String, val timestamp: Long, diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index 4314fe1c81..9e82c4a39f 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -26,6 +26,9 @@ data class CryptoCurrencySwapInfo( * @property providerId provider id * @property rateTypes supported rate types * @property isRecommended flag that indicates if this provider is recommended + * + * Uses to store transaction data in datastore, when extends - should always add default value + * to support backward compatibility */ data class SwapProvider( val providerId: String, @@ -35,7 +38,7 @@ data class SwapProvider( val imageLarge: String, val termsOfUse: String?, val privacyPolicy: String?, - val isRecommended: Boolean, + val isRecommended: Boolean = false, ) enum class ExchangeProviderType(val providerName: String) { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index eb1639b60d..df53e5e0e7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -896,8 +896,9 @@ internal class SwapInteractorImpl @Inject constructor( txExternalUrl: String? = null, txExternalId: String? = null, ) { + val selectedWallet = getSelectedWallet() ?: return swapTransactionRepository.storeTransaction( - userWalletId = UserWalletId(userWalletManager.getWalletId()), + userWalletId = selectedWallet.walletId, fromCryptoCurrency = currencyToSend.currency, toCryptoCurrency = currencyToGet.currency, transaction = SavedSwapTransactionModel( From 66f067505856be3099159e718c396f148ca8b35f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Jul 2024 19:12:46 +0100 Subject: [PATCH 36/48] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index ee09df73c4..27b0d03e63 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -430,8 +430,9 @@ internal class SwapViewModel @Inject constructor( if (currentSelected != null && consideredProviders.keys.contains(currentSelected)) { currentSelected } else { - findBestQuoteProvider(consideredProviders.getLastLoadedSuccessStates()) - ?: consideredProviders.keys.first() + val successLoadedData = consideredProviders.getLastLoadedSuccessStates() + val recommendedProvider = successLoadedData.keys.firstOrNull { it.isRecommended } + recommendedProvider ?: findBestQuoteProvider(successLoadedData) ?: consideredProviders.keys.first() } } else { state.keys.first() From 9331b0f7d7c29dc6d48520eebc31caa24a62999c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Jul 2024 19:53:16 +0100 Subject: [PATCH 37/48] Updated on 2026-08-14 --- .../feature/swap/analytics/SwapEvents.kt | 29 +++++++++++++++---- .../feature/swap/viewmodels/SwapViewModel.kt | 21 +++++++++++++- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 2fd8fa08ac..59fd338f5a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -6,6 +6,7 @@ import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.ApproveType private const val SWAP_CATEGORY = "Swap" +private const val PROMO_CATEGORY = "Promo" sealed class SwapEvents( event: String, @@ -17,7 +18,7 @@ sealed class SwapEvents( params = mapOf("Token" to token), ) - object SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") + data object SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") data class ChooseTokenScreenOpened(val availableTokens: Boolean) : SwapEvents( event = "Choose Token Screen Opened", @@ -37,7 +38,7 @@ sealed class SwapEvents( params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken), ) - object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission") + data object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission") data class ButtonPermissionApproveClicked( val sendToken: String, @@ -52,9 +53,9 @@ sealed class SwapEvents( ), ) - object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel") + data object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel") - object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") + data object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") data class SwapInProgressScreen( val provider: SwapProvider, @@ -71,7 +72,7 @@ sealed class SwapEvents( ), ) - object ProviderClicked : SwapEvents("Provider Clicked") + data object ProviderClicked : SwapEvents("Provider Clicked") data class ProviderChosen(val provider: SwapProvider) : SwapEvents( event = "Provider Chosen", @@ -88,7 +89,7 @@ sealed class SwapEvents( params = mapOf("Token" to token), ) - object NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap") + data object NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap") data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents( event = "Notice - Not Enough Fee", @@ -110,4 +111,20 @@ sealed class SwapEvents( "Error code" to errorCode.toString(), ), ) + + // region Promo activity + data class ChangellyActivity( + val promoState: PromoState, + ) : AnalyticsEvent( + category = PROMO_CATEGORY, + event = "Changelly Activity", + params = mapOf( + "State" to promoState.name, + ), + ) { + sealed class PromoState(val name: String) { + data object Native : PromoState("Native") + data object Recommended : PromoState("Recommended") + } + } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 27b0d03e63..58731f8a00 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -432,7 +432,9 @@ internal class SwapViewModel @Inject constructor( } else { val successLoadedData = consideredProviders.getLastLoadedSuccessStates() val recommendedProvider = successLoadedData.keys.firstOrNull { it.isRecommended } - recommendedProvider ?: findBestQuoteProvider(successLoadedData) ?: consideredProviders.keys.first() + val bestQuotesProvider = findBestQuoteProvider(successLoadedData) + triggerPromoProviderEvent(recommendedProvider, bestQuotesProvider) + recommendedProvider ?: bestQuotesProvider ?: consideredProviders.keys.first() } } else { state.keys.first() @@ -1170,11 +1172,28 @@ internal class SwapViewModel @Inject constructor( ) } + private fun triggerPromoProviderEvent(recommendedProvider: SwapProvider?, bestQuotesProvider: SwapProvider?) { + // for now send event only for changelly + if (recommendedProvider == null || + recommendedProvider.providerId != CHANGELLY_PROVIDER_ID || + bestQuotesProvider == null + ) { + return + } + val event = if (recommendedProvider.providerId == bestQuotesProvider.providerId) { + SwapEvents.ChangellyActivity(SwapEvents.ChangellyActivity.PromoState.Native) + } else { + SwapEvents.ChangellyActivity(SwapEvents.ChangellyActivity.PromoState.Recommended) + } + analyticsEventHandler.send(event = event) + } + private companion object { const val loggingTag = "SwapViewModel" const val INITIAL_AMOUNT = "" const val UPDATE_DELAY = 10000L const val DEBOUNCE_AMOUNT_DELAY = 1000L const val UPDATE_BALANCE_DELAY_MILLIS = 11000L + const val CHANGELLY_PROVIDER_ID = "changelly" } } \ No newline at end of file From b6a69d7e697a6e49571c4bf2b613b3bcfa6d1e7a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 12:09:47 +0100 Subject: [PATCH 38/48] Updated on 2026-08-14 --- .../presentation/wallet/domain/UserWalletExt.kt | 5 +++-- .../wallet/domain/WalletAdditionalInfoFactory.kt | 15 --------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt index ab7e236835..a78ded4bf0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt @@ -7,8 +7,9 @@ fun UserWallet.getCardsCount(): Int? { return if (isMultiCurrency) { when (val status = scanResponse.card.backupStatus) { is CardDTO.BackupStatus.Active -> status.cardCount + 1 - is CardDTO.BackupStatus.CardLinked -> status.cardCount + 1 - is CardDTO.BackupStatus.NoBackup -> 1 + is CardDTO.BackupStatus.NoBackup, + is CardDTO.BackupStatus.CardLinked, + -> 1 null -> 1 // Multi-currency wallet without backup function. Example, 4.12 } } else { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 3c61413f63..1f34e460f2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -6,7 +6,6 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo @@ -108,18 +107,4 @@ internal object WalletAdditionalInfoFactory { WalletAdditionalInfo(hideable = true, content = TextReference.Str(value = amount.orEmpty())) } } - - private fun UserWallet.getCardsCount(): Int? { - return if (isMultiCurrency) { - when (val status = scanResponse.card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount + 1 - is CardDTO.BackupStatus.CardLinked, - is CardDTO.BackupStatus.NoBackup, - null, - -> 1 - } - } else { - null - } - } } \ No newline at end of file From 90486a54fce52ca1e3f18245f5a0c7c3e6635ed5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 13:59:55 +0400 Subject: [PATCH 39/48] Updated on 2026-08-14 --- .../impl/domain/CustomTokenInteractor.kt | 2 +- .../domain/DefaultCustomTokenInteractor.kt | 23 +++++++++-------- .../viewmodels/AddCustomTokenViewModel.kt | 25 +++++++++++++------ .../tokens/AddCryptoCurrenciesUseCase.kt | 21 ++++++++-------- .../domain/tokens/error/AddCurrencyError.kt | 6 ----- 5 files changed, 41 insertions(+), 36 deletions(-) delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/AddCurrencyError.kt diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt index 16a0cb98a5..22bea5bf11 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt @@ -17,5 +17,5 @@ interface CustomTokenInteractor { /** Save token [customCurrency] */ @Throws(TangemError::class) - suspend fun saveToken(customCurrency: CustomCurrency) + suspend fun saveToken(customCurrency: CustomCurrency): Result } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt index de73abac25..ad210d7625 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt @@ -1,5 +1,7 @@ package com.tangem.tap.features.customtoken.impl.domain +import arrow.core.getOrElse +import arrow.core.raise.result import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.tokens.utils.CryptoCurrencyFactory @@ -15,7 +17,6 @@ import com.tangem.tap.domain.model.Currency import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store -import timber.log.Timber /** * Default implementation of custom token interactor @@ -45,16 +46,18 @@ class DefaultCustomTokenInteractor( ) } - override suspend fun saveToken(customCurrency: CustomCurrency) { - val userWallet = getSelectedWalletSyncUseCase().fold(ifLeft = { return }, ifRight = { it }) - val currency = Currency.fromCustomCurrency(customCurrency) - - val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse)) - derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies) - .onRight { - addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies) + override suspend fun saveToken(customCurrency: CustomCurrency): Result { + return result { + val userWallet = getSelectedWalletSyncUseCase().getOrElse { + error("Failed to get selected wallet: $it") } - .onLeft { Timber.e("Failed to derive public keys: $it") } + + val currency = Currency.fromCustomCurrency(customCurrency) + val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse)) + + derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies).bind() + addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies).bind() + } } private fun Currency.toCryptoCurrency(scanResponse: ScanResponse): CryptoCurrency? { diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index d725069217..5f523da11c 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -886,17 +886,26 @@ internal class AddCustomTokenViewModel @Inject constructor( analyticsSender.sendWhenAddTokenButtonClicked(currency) - viewModelScope.launch(dispatchers.io) { - val oldButtonState = uiState.floatingButton + viewModelScope.launch { uiState = uiState.copySealed( - floatingButton = uiState.floatingButton.copy(isEnabled = false, showProgress = true), + floatingButton = uiState.floatingButton.copy( + isEnabled = false, + showProgress = true, + ), ) - runCatching { featureInteractor.saveToken(currency) } + + val result = featureInteractor.saveToken(currency) + + uiState = uiState.copySealed( + floatingButton = uiState.floatingButton.copy( + isEnabled = true, + showProgress = false, + ), + ) + + result .onSuccess { featureRouter.openWalletScreen() } - .onFailure { - uiState = uiState.copySealed(floatingButton = oldButtonState) - Timber.e(it) - } + .onFailure { Timber.e(it, "Unable to save custom token") } } } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index 9febf735fe..e8c6f0b7a8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -5,7 +5,6 @@ import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.toNonEmptyListOrNull -import com.tangem.domain.tokens.error.AddCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -32,9 +31,9 @@ class AddCryptoCurrenciesUseCase( * * @param userWalletId The ID of the user's wallet. * @param currency Cryptocurrency to add. - * @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation. + * @return Either an [Throwable] or [Unit] indicating the success of the operation. */ - suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either { + suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either { return invoke(userWalletId, listOf(currency)) } @@ -47,13 +46,13 @@ class AddCryptoCurrenciesUseCase( * @param userWalletId The ID of the user's wallet. * @param cryptoCurrency Token to add. * @param network Network where we add - * @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation. + * @return Either an [Throwable] or [Unit] indicating the success of the operation. */ suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency.Token, network: Network, - ): Either = either { + ): Either = either { val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency = cryptoCurrency, network = network) invoke(userWalletId = userWalletId, currencies = listOf(tokenToAdd)) } @@ -66,14 +65,14 @@ class AddCryptoCurrenciesUseCase( * * @param userWalletId The ID of the user's wallet. * @param currencies The list of cryptocurrencies to add. - * @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation. + * @return Either an [Throwable] or [Unit] indicating the success of the operation. */ suspend operator fun invoke( userWalletId: UserWalletId, currencies: List, - ): Either = either { + ): Either = either { val existingCurrencies = catch({ currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }) { - raise(AddCurrencyError.DataError(it)) + raise(it) } val currenciesToAdd = currencies .filterNot(existingCurrencies::contains) @@ -81,7 +80,7 @@ class AddCryptoCurrenciesUseCase( ?: return@either catch({ currenciesRepository.addCurrencies(userWalletId, currenciesToAdd) }) { - raise(AddCurrencyError.DataError(it)) + raise(it) } refreshUpdatedNetworks(userWalletId, currenciesToAdd, existingCurrencies) @@ -91,7 +90,7 @@ class AddCryptoCurrenciesUseCase( * Refreshes the network statuses for tokens that have corresponding coins in the * [existingCurrencies] list. */ - private suspend fun Raise.refreshUpdatedNetworks( + private suspend fun Raise.refreshUpdatedNetworks( userWalletId: UserWalletId, currenciesToAdd: List, existingCurrencies: List, @@ -114,7 +113,7 @@ class AddCryptoCurrenciesUseCase( ) }, ) { - raise(AddCurrencyError.DataError(it)) + raise(it) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/AddCurrencyError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/AddCurrencyError.kt deleted file mode 100644 index 87255e52f7..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/AddCurrencyError.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.domain.tokens.error - -sealed class AddCurrencyError { - - data class DataError(val cause: Throwable) : AddCurrencyError() -} \ No newline at end of file From 8b247ae29128fae69f214e6f8946249206dc3055 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 15:46:58 +0400 Subject: [PATCH 40/48] Updated on 2026-08-14 --- .../domain/card/DefaultResetCardUseCase.kt | 61 +++++++++++-------- .../domain/card/models/ResetCardError.kt | 6 +- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt index 244dd02b7b..966f9613e9 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt @@ -1,15 +1,15 @@ package com.tangem.tap.domain.card import arrow.core.Either -import arrow.core.left +import arrow.core.raise.Raise import arrow.core.raise.either -import arrow.core.right +import arrow.fx.coroutines.ResourceScope +import arrow.fx.coroutines.resourceScope import com.tangem.common.CompletionResult import com.tangem.common.UserCodeType import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.core.UserCodeRequestPolicy -import com.tangem.common.doOnResult import com.tangem.domain.card.ResetCardUseCase import com.tangem.domain.card.models.ResetCardError import com.tangem.domain.models.scan.CardDTO @@ -20,36 +20,44 @@ internal class DefaultResetCardUseCase( private val tangemSdkManager: TangemSdkManager, ) : ResetCardUseCase { - override suspend fun invoke(card: CardDTO): Either = either { - enterRequiredAccessCode(card) { - resetToFactorySettings( + override suspend fun invoke(card: CardDTO): Either = resourceScope { + either { + withUserCodeRequestPolicy(card) + + tangemSdkManager.resetToFactorySettings( cardId = card.cardId, allowsRequestAccessCodeFromRepository = true, - ) + ).bind(raise = this) } - .mapToEither() } override suspend fun invoke( cardNumber: Int, card: CardDTO, userWalletId: UserWalletId, - ): Either = either { - enterRequiredAccessCode(card) { - resetBackupCard(cardNumber = cardNumber, userWalletId = userWalletId) + ): Either = resourceScope { + either { + withUserCodeRequestPolicy(card) + + tangemSdkManager.resetBackupCard( + cardNumber = cardNumber, + userWalletId = userWalletId, + ).bind(raise = this) } - .mapToEither() } - private suspend fun enterRequiredAccessCode( - card: CardDTO, - task: suspend TangemSdkManager.() -> CompletionResult<*>, - ): CompletionResult<*> { - val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy - requestMandatoryAccessCodeEntry(card) + private suspend fun ResourceScope.withUserCodeRequestPolicy(card: CardDTO) { + install( + acquire = { + val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy + requestMandatoryAccessCodeEntry(card) - return tangemSdkManager.task() - .doOnResult { tangemSdkManager.setUserCodeRequestPolicy(policyBeforeReset) } + policyBeforeReset + }, + release = { prevPolicy, _ -> + tangemSdkManager.setUserCodeRequestPolicy(prevPolicy) + }, + ) } private fun requestMandatoryAccessCodeEntry(card: CardDTO) { @@ -66,14 +74,19 @@ internal class DefaultResetCardUseCase( } } - private fun CompletionResult<*>.mapToEither(): Either { + private fun CompletionResult<*>.bind(raise: Raise) { return when (this) { - is CompletionResult.Failure -> error.mapToDomainError().left() - is CompletionResult.Success -> Unit.right() + is CompletionResult.Failure -> { + val domainError = error.mapToDomainError() + + raise.raise(domainError) + } + is CompletionResult.Success -> { /* no-op */ + } } } private fun TangemError.mapToDomainError(): ResetCardError { - return if (this is TangemSdkError.UserCancelled) ResetCardError.UserCanceled else ResetCardError.AnotherSdkError + return if (this is TangemSdkError.UserCancelled) ResetCardError.UserCanceled else ResetCardError.SdkError } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt index 7db8690e89..eb3c61288d 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt @@ -1,8 +1,8 @@ package com.tangem.domain.card.models -sealed interface ResetCardError { +sealed class ResetCardError { - data object UserCanceled : ResetCardError + data object UserCanceled : ResetCardError() - data object AnotherSdkError : ResetCardError + data object SdkError : ResetCardError() } \ No newline at end of file From 3d07a7dd18222831105924c36abbd9925aea0a60 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 15:58:11 +0400 Subject: [PATCH 41/48] Updated on 2026-08-14 --- .../send/impl/presentation/viewmodel/SendViewModel.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 456d91b7c6..30ce55fb24 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -894,13 +894,16 @@ internal class SendViewModel @Inject constructor( } private suspend fun sendTransaction(txData: TransactionData) { - sendTransactionUseCase( + val result = sendTransactionUseCase( txData = txData, userWallet = userWallet, network = cryptoCurrency.network, - ).fold( + ) + + uiState = stateFactory.getSendingStateUpdate(isSending = false) + + result.fold( ifLeft = { error -> - uiState = stateFactory.getSendingStateUpdate(isSending = false) uiState = eventStateFactory.getSendTransactionErrorState( error = error, onConsume = { uiState = eventStateFactory.onConsumeEventState() }, @@ -908,7 +911,6 @@ internal class SendViewModel @Inject constructor( analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol)) }, ifRight = { - uiState = stateFactory.getSendingStateUpdate(isSending = false) updateTransactionStatus(txData) addTokenToWalletIfNeeded() scheduleUpdates() From c9af7551a77e7ce01bf3bb6943c1bcfd5d4a5cf2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 16:32:03 +0400 Subject: [PATCH 42/48] Updated on 2026-08-14 --- .../send/impl/presentation/SendFragment.kt | 6 +- .../presentation/viewmodel/SendViewModel.kt | 187 +++++++++--------- 2 files changed, 98 insertions(+), 95 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 1c7d7f9cee..4eb34fce88 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation import android.os.Bundle import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -61,8 +62,11 @@ internal class SendFragment : ComposeFragment() { SystemBarsEffect { setSystemBarsColor(systemBarsColor) } + val currentState = viewModel.stateRouter.currentState.collectAsStateWithLifecycle() - SendScreen(viewModel.uiState, currentState.value) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + SendScreen(uiState, currentState.value) } override fun onDestroy() { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 30ce55fb24..622ca294c5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -1,9 +1,6 @@ package com.tangem.features.send.impl.presentation.viewmodel import android.os.SystemClock -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.* import arrow.core.Either import arrow.core.getOrElse @@ -123,7 +120,7 @@ internal class SendViewModel @Inject constructor( private val stateFactory = SendStateFactory( clickIntents = this, stateRouterProvider = Provider { stateRouter }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, @@ -133,7 +130,7 @@ internal class SendViewModel @Inject constructor( private val recipientStateFactory = RecipientSendFactory( stateRouterProvider = Provider { stateRouter }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, isUtxoConsolidationAvailableProvider = Provider { isUtxoConsolidationAvailable }, validateWalletMemoUseCase = validateWalletMemoUseCase, @@ -141,14 +138,14 @@ internal class SendViewModel @Inject constructor( private val amountStateFactory = AmountStateFactory( stateRouterProvider = Provider { stateRouter }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, ) private val feeStateFactory = FeeStateFactory( clickIntents = this, stateRouterProvider = Provider { stateRouter }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), isFeeApproximateUseCase = isFeeApproximateUseCase, @@ -157,13 +154,13 @@ internal class SendViewModel @Inject constructor( private val eventStateFactory = SendEventStateFactory( clickIntents = this, stateRouterProvider = Provider { stateRouter }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, feeStateFactory = feeStateFactory, ) private val feeNotificationFactory = FeeNotificationFactory( - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, stateRouterProvider = Provider { stateRouter }, clickIntents = this, ) @@ -171,7 +168,7 @@ internal class SendViewModel @Inject constructor( private val sendNotificationFactory = SendNotificationFactory( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, userWalletProvider = Provider { userWallet }, stateRouterProvider = Provider { stateRouter }, isSubtractAvailableProvider = Provider { isAmountSubtractAvailable }, @@ -186,15 +183,16 @@ internal class SendViewModel @Inject constructor( private val sendScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) { SendScreenAnalyticSender( stateRouterProvider = Provider { stateRouter }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, analyticsEventHandler = analyticsEventHandler, cryptoCurrencyProvider = Provider { cryptoCurrency }, ) } // todo convert to StateFlow - var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState()) - private set + val uiState: MutableStateFlow = MutableStateFlow( + value = stateFactory.getInitialState(), + ) private var userWallet: UserWallet by Delegates.notNull() private var userWallets: List = emptyList() @@ -272,7 +270,7 @@ internal class SendViewModel @Inject constructor( .conflate() .distinctUntilChanged() .onEach { - uiState = stateFactory.getOnHideBalanceState(isBalanceHidden = it.isBalanceHidden) + uiState.value = stateFactory.getOnHideBalanceState(isBalanceHidden = it.isBalanceHidden) } .launchIn(viewModelScope) .saveIn(balanceHidingJobHolder) @@ -373,15 +371,15 @@ internal class SendViewModel @Inject constructor( feeCryptoCurrencyStatus = feeCurrencyStatus subscribeOnQRScannerResult() when { - uiState.sendState?.isSuccess == true -> return + uiState.value.sendState?.isSuccess == true -> return transactionId != null && amount != null && destinationAddress != null -> { loadFee() - uiState = stateFactory.getReadyState(amount, destinationAddress, memo) + uiState.value = stateFactory.getReadyState(amount, destinationAddress, memo) stateRouter.showSend() updateNotifications() } else -> { - uiState = stateFactory.getReadyState() + uiState.value = stateFactory.getReadyState() getWalletsAndRecent() stateRouter.showRecipient() updateNotifications() @@ -405,9 +403,9 @@ internal class SendViewModel @Inject constructor( } }.onSuccess { result -> userWallets = result - uiState = recipientStateFactory.onLoadedWalletsList(wallets = userWallets) + uiState.value = recipientStateFactory.onLoadedWalletsList(wallets = userWallets) }.onFailure { - uiState = recipientStateFactory.onLoadedWalletsList(wallets = emptyList()) + uiState.value = recipientStateFactory.onLoadedWalletsList(wallets = emptyList()) } } } @@ -445,7 +443,7 @@ internal class SendViewModel @Inject constructor( pageSize = RECENT_TX_SIZE, ).getOrElse { emptyList() } } - uiState = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList) + uiState.value = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList) } private fun onStateActive() { @@ -456,7 +454,7 @@ internal class SendViewModel @Inject constructor( SendUiStateType.EditFee, -> loadFee() SendUiStateType.Send -> { - uiState = stateFactory.getIsAmountSubtractedState(isAmountSubtractAvailable) + uiState.value = stateFactory.getIsAmountSubtractedState(isAmountSubtractAvailable) sendIdleTimer = SystemClock.elapsedRealtime() } else -> Unit @@ -469,7 +467,7 @@ internal class SendViewModel @Inject constructor( sendNotificationFactory.create() .conflate() .distinctUntilChanged() - .onEach { uiState = stateFactory.getSendNotificationState(notifications = it) } + .onEach { uiState.value = stateFactory.getSendNotificationState(notifications = it) } .flowOn(dispatchers.main) .launchIn(viewModelScope) .saveIn(sendNotificationsJobHolder) @@ -479,7 +477,7 @@ internal class SendViewModel @Inject constructor( feeNotificationFactory.create() .conflate() .distinctUntilChanged() - .onEach { uiState = feeStateFactory.getFeeNotificationState(notifications = it) } + .onEach { uiState.value = feeStateFactory.getFeeNotificationState(notifications = it) } .flowOn(dispatchers.main) .launchIn(viewModelScope) .saveIn(feeNotificationsJobHolder) @@ -489,7 +487,7 @@ internal class SendViewModel @Inject constructor( override fun popBackStack() = stateRouter.popBackStack() override fun onBackClick() { cancelFeeRequest() - stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true) + stateRouter.onBackClick(isSuccess = uiState.value.sendState?.isSuccess == true) } override fun onCloseClick() { @@ -505,8 +503,8 @@ internal class SendViewModel @Inject constructor( override fun onNextClick(isFromEdit: Boolean) { val currentState = stateRouter.currentState.value - uiState = stateFactory.syncEditStates(isFromEdit = isFromEdit) - sendScreenAnalyticSender.send(currentState.type, uiState) + uiState.value = stateFactory.syncEditStates(isFromEdit = isFromEdit) + sendScreenAnalyticSender.send(currentState.type, uiState.value) when (currentState.type) { SendUiStateType.Fee, SendUiStateType.EditFee, @@ -531,9 +529,9 @@ internal class SendViewModel @Inject constructor( } override fun onFailedTxEmailClick(errorMessage: String) { - val recipient = uiState.recipientState?.addressTextField?.value - val feeValue = uiState.feeState?.fee?.amount?.value - val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value + val recipient = uiState.value.recipientState?.addressTextField?.value + val feeValue = uiState.value.feeState?.fee?.amount?.value + val amountValue = uiState.value.amountState?.amountTextField?.cryptoAmount?.value val receivingAmount = if (amountValue != null && feeValue != null) { checkAndCalculateSubtractedAmount( @@ -541,7 +539,7 @@ internal class SendViewModel @Inject constructor( cryptoCurrencyStatus = cryptoCurrencyStatus, amountValue = amountValue, feeValue = feeValue, - reduceAmountBy = uiState.sendState?.reduceAmountBy ?: BigDecimal.ZERO, + reduceAmountBy = uiState.value.sendState?.reduceAmountBy ?: BigDecimal.ZERO, ) } else { null @@ -562,20 +560,20 @@ internal class SendViewModel @Inject constructor( innerRouter.openTokenDetails(userWalletId, currency) private fun onFeeNext(): Boolean { - val feeState = uiState.getFeeState(stateRouter.isEditState) + val feeState = uiState.value.getFeeState(stateRouter.isEditState) val feeSelectorState = feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false if (checkIfFeeTooLow(feeSelectorState)) { - uiState = eventStateFactory.getFeeTooLowAlert( - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + uiState.value = eventStateFactory.getFeeTooLowAlert( + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) return true } return checkIfFeeTooHigh( feeSelectorState = feeSelectorState, onShow = { diff -> - uiState = eventStateFactory.getFeeTooHighAlert( + uiState.value = eventStateFactory.getFeeTooHighAlert( diff = diff, - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) }, ) @@ -607,20 +605,20 @@ internal class SendViewModel @Inject constructor( // region amount state clicks override fun onCurrencyChangeClick(isFiat: Boolean) { - uiState = amountStateFactory.getOnCurrencyChangedState(isFiat) + uiState.value = amountStateFactory.getOnCurrencyChangedState(isFiat) } override fun onAmountValueChange(value: String) { - uiState = amountStateFactory.getOnAmountValueChange(value) + uiState.value = amountStateFactory.getOnAmountValueChange(value) } override fun onMaxValueClick() { - uiState = amountStateFactory.getOnMaxAmountClick() + uiState.value = amountStateFactory.getOnMaxAmountClick() analyticsEventHandler.send(SendAnalyticEvents.MaxAmountButtonClicked) } override fun onAmountPasteTriggerDismiss() { - uiState = amountStateFactory.getOnAmountPastedTriggerDismiss() + uiState.value = amountStateFactory.getOnAmountPastedTriggerDismiss() } // endregion @@ -629,10 +627,10 @@ internal class SendViewModel @Inject constructor( override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) { viewModelScope.launch { if (!checkIfXrpAddressValue(value)) { - uiState = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null) - uiState = recipientStateFactory.getOnRecipientAddressValidationStarted() + uiState.value = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null) + uiState.value = recipientStateFactory.getOnRecipientAddressValidationStarted() val isValidAddress = validateAddress(value) - uiState = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress) + uiState.value = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress) type?.let { analyticsEventHandler.send( SendAnalyticEvents.AddressEntered( @@ -649,11 +647,11 @@ internal class SendViewModel @Inject constructor( override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) { viewModelScope.launch { if (!checkIfXrpAddressValue(value)) { - uiState = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted) - uiState = recipientStateFactory.getOnRecipientAddressValidationStarted() - val recipientState = uiState.getRecipientState(stateRouter.isEditState) + uiState.value = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted) + uiState.value = recipientStateFactory.getOnRecipientAddressValidationStarted() + val recipientState = uiState.value.getRecipientState(stateRouter.isEditState) val maybeValidAddress = validateAddress(recipientState?.addressTextField?.value.orEmpty()) - uiState = recipientStateFactory.getOnRecipientMemoValidState(value, maybeValidAddress.isRight()) + uiState.value = recipientStateFactory.getOnRecipientMemoValidState(value, maybeValidAddress.isRight()) } }.saveIn(memoValidationJobHolder) } @@ -675,21 +673,22 @@ internal class SendViewModel @Inject constructor( private suspend fun checkIfXrpAddressValue(value: String): Boolean { return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress -> - uiState = + uiState.value = recipientStateFactory.onRecipientAddressValueChange(value, isXAddress = true, isValuePasted = true) - uiState = recipientStateFactory.getOnXAddressMemoState() + uiState.value = recipientStateFactory.getOnXAddressMemoState() val isValidAddress = validateAddress(decodedAddress.address) - uiState = recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress) + uiState.value = + recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress) true } ?: false } private fun onEnteredValidAddress(isNotValid: Boolean) { - uiState = recipientStateFactory.getHiddenRecentListState(isNotValid = isNotValid) + uiState.value = recipientStateFactory.getHiddenRecentListState(isNotValid = isNotValid) } private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) { - val memo = uiState.getRecipientState(stateRouter.isEditState)?.memoTextField?.value + val memo = uiState.value.getRecipientState(stateRouter.isEditState)?.memoTextField?.value val isValidMemo = validateMemo(memo) val isRecent = type == EnterAddressSource.RecentAddress @@ -701,7 +700,7 @@ internal class SendViewModel @Inject constructor( override fun feeReload() = loadFee() override fun onFeeSelectorClick(feeType: FeeType) { - uiState = feeStateFactory.onFeeSelectedState(feeType) + uiState.value = feeStateFactory.onFeeSelectedState(feeType) updateFeeNotifications() if (feeType == FeeType.Custom) { analyticsEventHandler.send(SendAnalyticEvents.CustomFeeButtonClicked) @@ -709,7 +708,7 @@ internal class SendViewModel @Inject constructor( } override fun onCustomFeeValueChange(index: Int, value: String) { - uiState = feeStateFactory.onCustomFeeValueChange(index, value) + uiState.value = feeStateFactory.onCustomFeeValueChange(index, value) updateFeeNotifications() } @@ -725,14 +724,14 @@ internal class SendViewModel @Inject constructor( private fun loadFee() { viewModelScope.launch { - val isShowStatus = uiState.feeState?.fee == null + val isShowStatus = uiState.value.feeState?.fee == null if (isShowStatus) { - uiState = feeStateFactory.onFeeOnLoadingState() + uiState.value = feeStateFactory.onFeeOnLoadingState() updateNotifications() } val result = callFeeUseCase()?.fold( ifRight = { - uiState = feeStateFactory.onFeeOnLoadedState(it) + uiState.value = feeStateFactory.onFeeOnLoadedState(it) sendIdleTimer = SystemClock.elapsedRealtime() }, ifLeft = { @@ -748,7 +747,7 @@ internal class SendViewModel @Inject constructor( } private fun onFeeLoadFailed(isShowStatus: Boolean) { - if (isShowStatus) uiState = feeStateFactory.onFeeOnErrorState() + if (isShowStatus) uiState.value = feeStateFactory.onFeeOnErrorState() } private suspend fun checkIfSubtractAvailable() { @@ -767,8 +766,8 @@ internal class SendViewModel @Inject constructor( private suspend fun callFeeUseCase(): Either? { val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation - val amountState = uiState.getAmountState(isFromConfirmation) ?: return null - val recipientState = uiState.getRecipientState(isFromConfirmation) ?: return null + val amountState = uiState.value.getAmountState(isFromConfirmation) ?: return null + val recipientState = uiState.value.getRecipientState(isFromConfirmation) ?: return null val amount = amountState.amountTextField.cryptoAmount.value ?: return null return getFeeUseCase.invoke( @@ -782,10 +781,10 @@ internal class SendViewModel @Inject constructor( // region send state clicks override fun onSendClick() { - val sendState = uiState.sendState ?: return + val sendState = uiState.value.sendState ?: return if (sendState.isSuccess) popBackStack() - uiState = stateFactory.getSendingStateUpdate(isSending = true) + uiState.value = stateFactory.getSendingStateUpdate(isSending = true) if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) { verifyAndSendTransaction() } else { @@ -795,22 +794,22 @@ internal class SendViewModel @Inject constructor( } override fun showAmount() { - uiState = stateFactory.syncEditStates(isFromEdit = false) + uiState.value = stateFactory.syncEditStates(isFromEdit = false) stateRouter.showAmount(isFromConfirmation = true) setNeverToShowTapHelp() analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount)) } override fun showRecipient() { - uiState = stateFactory.syncEditStates(isFromEdit = false) + uiState.value = stateFactory.syncEditStates(isFromEdit = false) stateRouter.showRecipient(isFromConfirmation = true) - uiState = stateFactory.getHiddenTapHelpState() + uiState.value = stateFactory.getHiddenTapHelpState() setNeverToShowTapHelp() analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address)) } override fun showFee() { - uiState = stateFactory.syncEditStates(isFromEdit = false) + uiState.value = stateFactory.syncEditStates(isFromEdit = false) stateRouter.showFee(isFromConfirmation = true) setNeverToShowTapHelp() analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) @@ -821,7 +820,7 @@ internal class SendViewModel @Inject constructor( } override fun onExploreClick() { - val sendState = uiState.sendState ?: return + val sendState = uiState.value.sendState ?: return analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked) innerRouter.openUrl(sendState.txUrl) } @@ -836,7 +835,7 @@ internal class SendViewModel @Inject constructor( reduceAmountTo: BigDecimal?, clazz: Class, ) { - uiState = when { + uiState.value = when { reduceAmountBy != null && reduceAmountByDiff != null -> amountStateFactory.getOnAmountReduceByState( reduceAmountBy = reduceAmountBy, reduceAmountByDiff = reduceAmountByDiff, @@ -845,20 +844,20 @@ internal class SendViewModel @Inject constructor( else -> return } - uiState = sendNotificationFactory.dismissNotificationState(clazz) + uiState.value = sendNotificationFactory.dismissNotificationState(clazz) updateNotifications() } override fun onNotificationCancel(clazz: Class) { - uiState = sendNotificationFactory.dismissNotificationState(clazz = clazz, isIgnored = true) + uiState.value = sendNotificationFactory.dismissNotificationState(clazz = clazz, isIgnored = true) } private fun verifyAndSendTransaction() { - val recipient = uiState.recipientState?.addressTextField?.value ?: return - val feeState = uiState.feeState ?: return + val recipient = uiState.value.recipientState?.addressTextField?.value ?: return + val feeState = uiState.value.feeState ?: return val fee = feeState.fee ?: return - val memo = uiState.recipientState?.memoTextField?.value - val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return + val memo = uiState.value.recipientState?.memoTextField?.value + val amountValue = uiState.value.amountState?.amountTextField?.cryptoAmount?.value ?: return val feeValue = fee.amount.value ?: return val receivingAmount = checkAndCalculateSubtractedAmount( @@ -866,7 +865,7 @@ internal class SendViewModel @Inject constructor( cryptoCurrencyStatus = cryptoCurrencyStatus, amountValue = amountValue, feeValue = feeValue, - reduceAmountBy = uiState.sendState?.reduceAmountBy ?: BigDecimal.ZERO, + reduceAmountBy = uiState.value.sendState?.reduceAmountBy ?: BigDecimal.ZERO, ) viewModelScope.launch { @@ -880,10 +879,10 @@ internal class SendViewModel @Inject constructor( ).fold( ifLeft = { Timber.e(it) - uiState = stateFactory.getSendingStateUpdate(isSending = false) - uiState = eventStateFactory.getGenericErrorState( + uiState.value = stateFactory.getSendingStateUpdate(isSending = false) + uiState.value = eventStateFactory.getGenericErrorState( error = it, - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) }, ifRight = { txData -> @@ -900,13 +899,13 @@ internal class SendViewModel @Inject constructor( network = cryptoCurrency.network, ) - uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState.value = stateFactory.getSendingStateUpdate(isSending = false) result.fold( ifLeft = { error -> - uiState = eventStateFactory.getSendTransactionErrorState( + uiState.value = eventStateFactory.getSendTransactionErrorState( error = error, - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol)) }, @@ -922,7 +921,7 @@ internal class SendViewModel @Inject constructor( private fun addTokenToWalletIfNeeded() { if (cryptoCurrency !is CryptoCurrency.Token) return - val recipientState = uiState.getRecipientState(stateRouter.isEditState) ?: return + val recipientState = uiState.value.getRecipientState(stateRouter.isEditState) ?: return val destinationAddress = recipientState.addressTextField.value val receivingUserWallet = userWallets.firstOrNull { it.address == destinationAddress } ?: return @@ -941,7 +940,7 @@ internal class SendViewModel @Inject constructor( userWalletId = userWalletId, network = cryptoCurrency.network, ).getOrElse { "" } - uiState = stateFactory.getTransactionSendState(txData, txUrl) + uiState.value = stateFactory.getTransactionSendState(txData, txUrl) } private fun scheduleUpdates() { @@ -959,7 +958,7 @@ internal class SendViewModel @Inject constructor( } private fun onCheckFeeUpdate() { - val sendState = uiState.sendState ?: return + val sendState = uiState.value.sendState ?: return val isSuccess = sendState.isSuccess val noErrorNotifications = sendState.notifications.none { it is SendNotification.Error } @@ -967,30 +966,30 @@ internal class SendViewModel @Inject constructor( viewModelScope.launch { val feeUpdatedState = callFeeUseCase()?.fold( ifRight = { - uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState.value = stateFactory.getSendingStateUpdate(isSending = false) eventStateFactory.getFeeUpdatedAlert( fee = it, - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, onFeeNotIncreased = { - uiState = stateFactory.getSendingStateUpdate(isSending = true) + uiState.value = stateFactory.getSendingStateUpdate(isSending = true) verifyAndSendTransaction() }, ) }, ifLeft = { - uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState.value = stateFactory.getSendingStateUpdate(isSending = false) eventStateFactory.getFeeUnreachableErrorState( - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) }, ) - uiState = if (feeUpdatedState != null) { + uiState.value = if (feeUpdatedState != null) { feeUpdatedState } else { - uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState.value = stateFactory.getSendingStateUpdate(isSending = false) eventStateFactory.getFeeUnreachableErrorState( - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) } } @@ -1001,12 +1000,12 @@ internal class SendViewModel @Inject constructor( viewModelScope.launch { neverShowTapHelpUseCase() } - uiState = stateFactory.getHiddenTapHelpState() + uiState.value = stateFactory.getHiddenTapHelpState() } private fun showErrorAlert() { - uiState = eventStateFactory.getGenericErrorState( - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + uiState.value = eventStateFactory.getGenericErrorState( + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) } // endregion From 2f325fa46f9b0303128dfe3f62d0188ef942e603 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 16:50:43 +0400 Subject: [PATCH 43/48] Updated on 2026-08-14 --- .../presentation/AddCustomTokenFragment.kt | 7 +- .../viewmodels/AddCustomTokenViewModel.kt | 539 ++++++++++-------- 2 files changed, 298 insertions(+), 248 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt index da295c57c5..f2b19a713d 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt @@ -2,9 +2,11 @@ package com.tangem.tap.features.customtoken.impl.presentation import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme @@ -34,9 +36,12 @@ internal class AddCustomTokenFragment : ComposeFragment() { SystemBarsEffect { setSystemBarsColor(color = statusBarColor) } + + val state by viewModel.uiState.collectAsStateWithLifecycle() + AddCustomTokenScreen( modifier = Modifier.systemBarsPadding(), - stateHolder = viewModel.uiState, + stateHolder = state, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 5f523da11c..3b9c1f3f5d 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -1,9 +1,6 @@ package com.tangem.tap.features.customtoken.impl.presentation.viewmodels import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.lifecycle.DefaultLifecycleObserver @@ -49,6 +46,9 @@ import com.tangem.wallet.R import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -85,8 +85,7 @@ internal class AddCustomTokenViewModel @Inject constructor( private var currentCryptoCurrencies: List = emptyList() /** Screen state */ - var uiState by mutableStateOf(getInitialUiState()) - private set + val uiState: MutableStateFlow = MutableStateFlow(value = getInitialUiState()) private var foundToken: FoundToken? = null @@ -376,44 +375,48 @@ internal class AddCustomTokenViewModel @Inject constructor( } .onSuccess { token -> foundToken = token - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy( - isLoading = false, - ), - networkSelectorField = uiState.form.networkSelectorField.copy( - selectedItem = formStateBuilder.createNetworkSelectorItem( - blockchain = Blockchain.fromNetworkId(token.network.id) - ?: Blockchain.Unknown, + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy( + isLoading = false, + ), + networkSelectorField = state.form.networkSelectorField.copy( + selectedItem = formStateBuilder.createNetworkSelectorItem( + blockchain = Blockchain.fromNetworkId(token.network.id) + ?: Blockchain.Unknown, + ), + ), + tokenNameInputField = state.form.tokenNameInputField.copy( + value = token.name, + isEnabled = false, + ), + tokenSymbolInputField = state.form.tokenSymbolInputField.copy( + value = token.symbol, + isEnabled = false, + ), + decimalsInputField = state.form.decimalsInputField.copy( + value = token.network.decimalCount, + isEnabled = false, ), ), - tokenNameInputField = uiState.form.tokenNameInputField.copy( - value = token.name, - isEnabled = false, - ), - tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy( - value = token.symbol, - isEnabled = false, - ), - decimalsInputField = uiState.form.decimalsInputField.copy( - value = token.network.decimalCount, - isEnabled = false, - ), - ), - ) + ) + } } .onFailure { foundToken = null - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy( - isLoading = false, + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy( + isLoading = false, + ), + tokenNameInputField = state.form.tokenNameInputField.copy(isEnabled = true), + tokenSymbolInputField = state.form.tokenSymbolInputField.copy(isEnabled = true), + decimalsInputField = state.form.decimalsInputField.copy(isEnabled = true), ), - tokenNameInputField = uiState.form.tokenNameInputField.copy(isEnabled = true), - tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(isEnabled = true), - decimalsInputField = uiState.form.decimalsInputField.copy(isEnabled = true), - ), - ) + ) + } Timber.e(it) } @@ -423,28 +426,31 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isDerivationPathSelected(): Boolean { - return with(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain) { - this != null && this != Blockchain.Unknown || - uiState.form.derivationPathSelectorField?.selectedItem?.type == DerivationPathSelectorType.CUSTOM && - !uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath) - } + val blockchain = uiState.value.form.derivationPathSelectorField?.selectedItem?.blockchain + val selectorType = uiState.value.form.derivationPathSelectorField?.selectedItem?.type + + return blockchain != null && blockchain != Blockchain.Unknown || + selectorType == DerivationPathSelectorType.CUSTOM && + !uiState.value.warnings.contains(AddCustomTokenWarning.WrongDerivationPath) } private fun updateWarnings() { - uiState = uiState.copySealed( - warnings = buildSet { - when (getCustomTokenType()) { - CustomTokenType.TOKEN -> { - addAll(getTokenWarningSet()) - } + uiState.update { state -> + state.copySealed( + warnings = buildSet { + when (getCustomTokenType()) { + CustomTokenType.TOKEN -> { + addAll(getTokenWarningSet()) + } - CustomTokenType.BLOCKCHAIN -> { - if (isCustomTokenAlreadyAdded()) add(AddCustomTokenWarning.TokenAlreadyAdded) - if (isDerivationPathSelected()) add(AddCustomTokenWarning.PotentialScamToken) + CustomTokenType.BLOCKCHAIN -> { + if (isCustomTokenAlreadyAdded()) add(AddCustomTokenWarning.TokenAlreadyAdded) + if (isDerivationPathSelected()) add(AddCustomTokenWarning.PotentialScamToken) + } } - } - }, - ) + }, + ) + } } private fun getCustomTokenType(): CustomTokenType { @@ -456,24 +462,24 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isAnyTokenFieldsFilled(): Boolean { - return with(uiState.form) { + return with(uiState.value.form) { contractAddressInputField.value.isNotEmpty() || tokenNameInputField.value.isNotEmpty() || tokenSymbolInputField.value.isNotEmpty() || decimalsInputField.value.isNotEmpty() } } private fun isAllTokenFieldsFilled(): Boolean { - return with(uiState.form) { + return with(uiState.value.form) { contractAddressInputField.value.isNotEmpty() && tokenNameInputField.value.isNotEmpty() && tokenSymbolInputField.value.isNotEmpty() && decimalsInputField.value.isNotEmpty() } } private fun getTokenWarningSet(): Set { - val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain + val networkSelectorValue = uiState.value.form.networkSelectorField.selectedItem.blockchain val isContractAddressFieldEmpty = ContractAddressValidator.validate( - address = uiState.form.contractAddressInputField.value, + address = uiState.value.form.contractAddressInputField.value, blockchain = networkSelectorValue, ).let { it is ContractAddressValidatorResult.Error && it.type == AddCustomTokenError.FIELD_IS_EMPTY @@ -507,66 +513,70 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isNetworkSelected(): Boolean { - return uiState.form.networkSelectorField.selectedItem.blockchain != Blockchain.Unknown + return uiState.value.form.networkSelectorField.selectedItem.blockchain != Blockchain.Unknown } private fun updateFloatingButton() { - uiState = updateStateWithDerivationError(uiState) + uiState.update { state -> + updateStateWithDerivationError(state) + } if (isCustomTokenAlreadyAdded()) { - uiState = uiState.copySealed( - warnings = uiState.warnings + AddCustomTokenWarning.TokenAlreadyAdded, - floatingButton = uiState.floatingButton.copy(isEnabled = false), - ) + uiState.update { state -> + state.copySealed( + warnings = state.warnings + AddCustomTokenWarning.TokenAlreadyAdded, + floatingButton = state.floatingButton.copy(isEnabled = false), + ) + } return } - val isCorrectDerivationInput = !uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath) - val state = when { - isAllTokenFieldsFilled() && isNetworkSelected() -> { - val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain - val error = ContractAddressValidator.validate( - address = uiState.form.contractAddressInputField.value, - blockchain = networkSelectorValue, - ) + uiState.update { state -> + val isCorrectDerivationInput = !state.warnings.contains(AddCustomTokenWarning.WrongDerivationPath) + val updatedState = when { + isAllTokenFieldsFilled() && isNetworkSelected() -> { + val networkSelectorValue = state.form.networkSelectorField.selectedItem.blockchain + val error = ContractAddressValidator.validate( + address = state.form.contractAddressInputField.value, + blockchain = networkSelectorValue, + ) - val isSupportedToken = getSelectedWalletSyncUseCase().fold( - ifLeft = { false }, - ifRight = { - it.scanResponse.card.canHandleToken( - blockchain = networkSelectorValue, - cardTypesResolver = it.scanResponse.cardTypesResolver, - ) - }, - ) - - uiState.copySealed( - floatingButton = uiState.floatingButton.copy( - isEnabled = error is ContractAddressValidatorResult.Success && - isSupportedToken && isCorrectDerivationInput, - ), - ) - } - - isAnyTokenFieldsFilled() -> { - uiState.copySealed(floatingButton = uiState.floatingButton.copy(isEnabled = false)) - } - - else -> { - uiState.copySealed( - floatingButton = uiState.floatingButton.copy( - isEnabled = if (isNetworkSelected()) { - !isBlockchainAlreadyAdded() && isCorrectDerivationInput - } else { - false + val isSupportedToken = getSelectedWalletSyncUseCase().fold( + ifLeft = { false }, + ifRight = { + it.scanResponse.card.canHandleToken( + blockchain = networkSelectorValue, + cardTypesResolver = it.scanResponse.cardTypesResolver, + ) }, - ), - ) - } - } + ) - uiState = state.copySealed( - warnings = state.warnings - AddCustomTokenWarning.TokenAlreadyAdded, - ) + state.copySealed( + floatingButton = state.floatingButton.copy( + isEnabled = error is ContractAddressValidatorResult.Success && + isSupportedToken && isCorrectDerivationInput, + ), + ) + } + isAnyTokenFieldsFilled() -> { + state.copySealed(floatingButton = state.floatingButton.copy(isEnabled = false)) + } + else -> { + state.copySealed( + floatingButton = state.floatingButton.copy( + isEnabled = if (isNetworkSelected()) { + !isBlockchainAlreadyAdded() && isCorrectDerivationInput + } else { + false + }, + ), + ) + } + } + + updatedState.copySealed( + warnings = updatedState.warnings - AddCustomTokenWarning.TokenAlreadyAdded, + ) + } } private fun updateStateWithDerivationError(uiState: AddCustomTokenStateHolder): AddCustomTokenStateHolder { @@ -599,11 +609,11 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isTokenAlreadyAdded(): Boolean { - val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain + val networkSelectorValue = uiState.value.form.networkSelectorField.selectedItem.blockchain val networkId = Blockchain.fromNetworkId(networkSelectorValue.toNetworkId())?.id val contractAddress = convertTokenAddress( blockchain = networkSelectorValue, - address = uiState.form.contractAddressInputField.value, + address = uiState.value.form.contractAddressInputField.value, ) ?: return false // invalid address can't be "already added" return currentCryptoCurrencies @@ -628,7 +638,7 @@ internal class AddCustomTokenViewModel @Inject constructor( return currentCryptoCurrencies .filterIsInstance() .any { coin -> - coin.network.id.value == uiState.form.networkSelectorField.selectedItem.blockchain.id && + coin.network.id.value == uiState.value.form.networkSelectorField.selectedItem.blockchain.id && coin.network.derivationPath.value == getDerivationPath()?.rawPath } } @@ -637,39 +647,43 @@ internal class AddCustomTokenViewModel @Inject constructor( when { isNetworkSelected() && type == AddCustomTokenError.INVALID_CONTRACT_ADDRESS -> { val isAnotherTokenFieldsFilled = isAnyTokenFieldsFilled() - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy( - isError = true, - error = TextReference.Res( - id = R.string.custom_token_creation_error_invalid_contract_address, + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy( + isError = true, + error = TextReference.Res( + id = R.string.custom_token_creation_error_invalid_contract_address, + ), + ), + tokenNameInputField = state.form.tokenNameInputField.copy( + isEnabled = isAnotherTokenFieldsFilled, + ), + tokenSymbolInputField = state.form.tokenSymbolInputField.copy( + isEnabled = isAnotherTokenFieldsFilled, + ), + decimalsInputField = state.form.decimalsInputField.copy( + isEnabled = isAnotherTokenFieldsFilled, ), ), - tokenNameInputField = uiState.form.tokenNameInputField.copy( - isEnabled = isAnotherTokenFieldsFilled, - ), - tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy( - isEnabled = isAnotherTokenFieldsFilled, - ), - decimalsInputField = uiState.form.decimalsInputField.copy( - isEnabled = isAnotherTokenFieldsFilled, - ), - ), - ) + ) + } } !isNetworkSelected() || type == AddCustomTokenError.FIELD_IS_EMPTY -> { - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy(isError = false), - tokenNameInputField = uiState.form.tokenNameInputField.copy(value = "", isEnabled = false), - tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy( - value = "", - isEnabled = false, + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy(isError = false), + tokenNameInputField = state.form.tokenNameInputField.copy(value = "", isEnabled = false), + tokenSymbolInputField = state.form.tokenSymbolInputField.copy( + value = "", + isEnabled = false, + ), + decimalsInputField = state.form.decimalsInputField.copy(value = "", isEnabled = false), ), - decimalsInputField = uiState.form.decimalsInputField.copy(value = "", isEnabled = false), - ), - ) + ) + } } else -> Unit @@ -677,11 +691,11 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun getDerivationPath(): DerivationPath? { - return when (uiState.form.derivationPathSelectorField?.selectedItem?.type) { + return when (uiState.value.form.derivationPathSelectorField?.selectedItem?.type) { DerivationPathSelectorType.CUSTOM -> - createDerivationPathOrNull(uiState.form.derivationPathInputField?.value ?: "") + createDerivationPathOrNull(uiState.value.form.derivationPathInputField?.value ?: "") else -> - getDerivationPathForBlockchain(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain) + getDerivationPathForBlockchain(uiState.value.form.derivationPathSelectorField?.selectedItem?.blockchain) } } @@ -704,7 +718,7 @@ internal class AddCustomTokenViewModel @Inject constructor( ) val derivationNetwork = if (blockchain == Blockchain.Unknown) { - uiState.form.networkSelectorField.selectedItem.blockchain + uiState.value.form.networkSelectorField.selectedItem.blockchain } else { blockchain } @@ -739,13 +753,15 @@ internal class AddCustomTokenViewModel @Inject constructor( } fun onContactAddressValueChange(enteredValue: String) { - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy(value = enteredValue), - ), - ) + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy(value = enteredValue), + ), + ) + } - val selectedNetwork = uiState.form.networkSelectorField.selectedItem.blockchain + val selectedNetwork = uiState.value.form.networkSelectorField.selectedItem.blockchain val validatorResult = ContractAddressValidator.validate( address = enteredValue, blockchain = selectedNetwork, @@ -753,14 +769,16 @@ internal class AddCustomTokenViewModel @Inject constructor( when (validatorResult) { is ContractAddressValidatorResult.Success -> { - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy( - isError = false, - isLoading = true, + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy( + isError = false, + isLoading = true, + ), ), - ), - ) + ) + } updateForm(address = enteredValue, selectedNetwork = selectedNetwork) } @@ -773,76 +791,92 @@ internal class AddCustomTokenViewModel @Inject constructor( } fun onNetworkSelectorItemClick(index: Int) { - val selectedItem = requireNotNull(uiState.form.networkSelectorField.items.getOrNull(index)) - uiState = uiState.copySealed( - form = uiState.form.copy( - networkSelectorField = uiState.form.networkSelectorField.copy( - selectedItem = selectedItem, + val state = uiState.updateAndGet { state -> + val selectedItem = requireNotNull(state.form.networkSelectorField.items.getOrNull(index)) + state.copySealed( + form = state.form.copy( + networkSelectorField = state.form.networkSelectorField.copy( + selectedItem = selectedItem, + ), + showTokenFields = selectedItem.blockchain.canHandleTokens() && + // workaround cause in Terra we support only 1 token + selectedItem.blockchain != Blockchain.TerraV1, ), - showTokenFields = selectedItem.blockchain.canHandleTokens() && - // workaround cause in Terra we support only 1 token - selectedItem.blockchain != Blockchain.TerraV1, - ), - ) - onContactAddressValueChange(uiState.form.contractAddressInputField.value) + ) + } + + onContactAddressValueChange(state.form.contractAddressInputField.value) } fun onTokenNameValueChange(enteredValue: String) { - uiState = uiState.copySealed( - form = uiState.form.copy( - tokenNameInputField = uiState.form.tokenNameInputField.copy(value = enteredValue), - ), - ) + uiState.update { state -> + state.copySealed( + form = state.form.copy( + tokenNameInputField = state.form.tokenNameInputField.copy(value = enteredValue), + ), + ) + } + updateFloatingButton() } fun onTokenSymbolValueChange(enteredValue: String) { - uiState = uiState.copySealed( - form = uiState.form.copy( - tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(value = enteredValue), - ), - ) + uiState.update { state -> + state.copySealed( + form = state.form.copy( + tokenSymbolInputField = state.form.tokenSymbolInputField.copy(value = enteredValue), + ), + ) + } updateFloatingButton() } fun onDecimalsValueChange(enteredValue: String) { - uiState = uiState.copySealed( - form = uiState.form.copy( - decimalsInputField = uiState.form.decimalsInputField.copy(value = enteredValue), - ), - ) + uiState.update { state -> + state.copySealed( + form = state.form.copy( + decimalsInputField = state.form.decimalsInputField.copy(value = enteredValue), + ), + ) + } updateFloatingButton() } fun onDerivationPathSelectorItemClick(index: Int) { - val derivationSelector = requireNotNull(uiState.form.derivationPathSelectorField) - val selected = requireNotNull(derivationSelector.items.getOrNull(index)) - val derivationInputField = requireNotNull(uiState.form.derivationPathInputField) - uiState = uiState.copySealed( - form = uiState.form.copy( - derivationPathSelectorField = derivationSelector.copy( - selectedItem = selected, + uiState.update { state -> + val derivationSelector = requireNotNull(state.form.derivationPathSelectorField) + val selected = requireNotNull(derivationSelector.items.getOrNull(index)) + val derivationInputField = requireNotNull(state.form.derivationPathInputField) + + state.copySealed( + form = state.form.copy( + derivationPathSelectorField = derivationSelector.copy( + selectedItem = selected, + ), + derivationPathInputField = derivationInputField.copy( + showField = selected.type == DerivationPathSelectorType.CUSTOM, + ), ), - derivationPathInputField = derivationInputField.copy( - showField = selected.type == DerivationPathSelectorType.CUSTOM, - ), - ), - ) + ) + } + updateFloatingButton() } fun onDerivationPathValueChange(enteredValue: String) { - uiState = uiState.copySealed( - form = uiState.form.copy( - derivationPathInputField = uiState.form.derivationPathInputField?.copy(value = enteredValue), - ), - ) + uiState.update { state -> + state.copySealed( + form = state.form.copy( + derivationPathInputField = state.form.derivationPathInputField?.copy(value = enteredValue), + ), + ) + } updateFloatingButton() } fun onAddCustomTokenClick() { if (!isNetworkSelected()) return - val blockchain = uiState.form.networkSelectorField.selectedItem.blockchain + val blockchain = uiState.value.form.networkSelectorField.selectedItem.blockchain when (getSupportBlockchainType(blockchain)) { SupportBlockchainType.SUPPORTED -> { /* no-op */ @@ -861,15 +895,16 @@ internal class AddCustomTokenViewModel @Inject constructor( CustomTokenType.TOKEN -> { val contractAddress = convertTokenAddress( blockchain = blockchain, - address = foundToken?.network?.contractAddress ?: uiState.form.contractAddressInputField.value, + address = foundToken?.network?.contractAddress ?: uiState.value.form.contractAddressInputField + .value, ) ?: error("Contract address is invalid") // impossible to add a token with invalid address CustomCurrency.CustomToken( token = Token( - name = uiState.form.tokenNameInputField.value, - symbol = uiState.form.tokenSymbolInputField.value, + name = uiState.value.form.tokenNameInputField.value, + symbol = uiState.value.form.tokenSymbolInputField.value, contractAddress = contractAddress, - decimals = requireNotNull(uiState.form.decimalsInputField.value.toIntOrNull()), + decimals = requireNotNull(uiState.value.form.decimalsInputField.value.toIntOrNull()), id = foundToken?.id, ), network = blockchain, @@ -887,21 +922,25 @@ internal class AddCustomTokenViewModel @Inject constructor( analyticsSender.sendWhenAddTokenButtonClicked(currency) viewModelScope.launch { - uiState = uiState.copySealed( - floatingButton = uiState.floatingButton.copy( - isEnabled = false, - showProgress = true, - ), - ) + uiState.update { state -> + state.copySealed( + floatingButton = state.floatingButton.copy( + isEnabled = false, + showProgress = true, + ), + ) + } val result = featureInteractor.saveToken(currency) - uiState = uiState.copySealed( - floatingButton = uiState.floatingButton.copy( - isEnabled = true, - showProgress = false, - ), - ) + uiState.update { state -> + state.copySealed( + floatingButton = state.floatingButton.copy( + isEnabled = true, + showProgress = false, + ), + ) + } result .onSuccess { featureRouter.openWalletScreen() } @@ -917,7 +956,7 @@ internal class AddCustomTokenViewModel @Inject constructor( Blockchain.Cardano -> { cardanoTokenAddressConverter.convertToFingerprint( address = address, - symbol = uiState.form.tokenSymbolInputField.value, + symbol = uiState.value.form.tokenSymbolInputField.value, ) } else -> address @@ -927,46 +966,52 @@ internal class AddCustomTokenViewModel @Inject constructor( private inner class TestActionsHandler { fun onClearAddressButtonClick() { - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy( - value = "", - isLoading = false, - isError = false, - error = null, - ), - ), - ) - } - - fun onResetButtonClick() { - with(uiState.form) { - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = contractAddressInputField.copy( + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy( value = "", isLoading = false, isError = false, error = null, ), - networkSelectorField = networkSelectorField.copy( - selectedItem = formStateBuilder.createNetworkSelectorItem(blockchain = Blockchain.Unknown), - ), - tokenNameInputField = tokenNameInputField.copy(value = "", isEnabled = false), - tokenSymbolInputField = tokenSymbolInputField.copy(value = "", isEnabled = false), - decimalsInputField = decimalsInputField.copy(value = "", isEnabled = false), - derivationPathSelectorField = derivationPathSelectorField?.copy( - isEnabled = true, - selectedItem = formStateBuilder.createDerivationPathSelectorAdditionalItem( - blockchain = Blockchain.Unknown, - type = DerivationPathSelectorType.DEFAULT, - derivationPath = "", - ), - ), ), ) } } + + fun onResetButtonClick() { + uiState.update { state -> + with(state.form) { + state.copySealed( + form = state.form.copy( + contractAddressInputField = contractAddressInputField.copy( + value = "", + isLoading = false, + isError = false, + error = null, + ), + networkSelectorField = networkSelectorField.copy( + selectedItem = formStateBuilder.createNetworkSelectorItem( + blockchain = Blockchain.Unknown, + ), + ), + tokenNameInputField = tokenNameInputField.copy(value = "", isEnabled = false), + tokenSymbolInputField = tokenSymbolInputField.copy(value = "", isEnabled = false), + decimalsInputField = decimalsInputField.copy(value = "", isEnabled = false), + derivationPathSelectorField = derivationPathSelectorField?.copy( + isEnabled = true, + selectedItem = formStateBuilder.createDerivationPathSelectorAdditionalItem( + blockchain = Blockchain.Unknown, + type = DerivationPathSelectorType.DEFAULT, + derivationPath = "", + ), + ), + ), + ) + } + } + } } private companion object { From 6025d8ab6796a06ce089c4943a03ed0d1913cb45 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 14:58:16 +0100 Subject: [PATCH 44/48] Updated on 2026-08-14 --- .../tokendetails/viewmodels/TokenDetailsViewModel.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index c2ff51d2d6..4d115db24e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -538,7 +538,13 @@ internal class TokenDetailsViewModel @Inject constructor( ifRight = { it }, ) if (extendedKey.isNotBlank()) { - router.share(extendedKey) + hapticManager.vibrateMeduim() + clipboardManager.setText(text = extendedKey) + internalUiState.value = stateFactory.getStateAndTriggerEvent( + state = internalUiState.value, + errorMessage = resourceReference(R.string.wallet_notification_address_copied), + setUiState = { internalUiState.value = it }, + ) } } } From e2c5e34762fefb14b32e872a45b9e32cd8ab7dc8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 17:57:59 +0300 Subject: [PATCH 45/48] Updated on 2026-08-14 --- .../viewmodels/AddCustomTokenViewModel.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 3b9c1f3f5d..df99426334 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -951,16 +951,20 @@ internal class AddCustomTokenViewModel @Inject constructor( /** Convert [address] to single address for specific [blockchain] or return null if invalid */ private fun convertTokenAddress(blockchain: Blockchain, address: String): String? { - return when (blockchain) { - Blockchain.Hedera, Blockchain.HederaTestnet -> hederaAddressConverter.convertToTokenId(address) - Blockchain.Cardano -> { - cardanoTokenAddressConverter.convertToFingerprint( - address = address, - symbol = uiState.value.form.tokenSymbolInputField.value, - ) + return runCatching { + when (blockchain) { + Blockchain.Hedera, Blockchain.HederaTestnet -> hederaAddressConverter.convertToTokenId(address) + Blockchain.Cardano -> { + // TODO: [REDACTED_JIRA] + cardanoTokenAddressConverter.convertToFingerprint( + address = address, + symbol = uiState.value.form.tokenSymbolInputField.value, + ) + } + else -> address } - else -> address } + .getOrNull() } private inner class TestActionsHandler { From c73e23d4d3103fffa606a6f1933247e728e060a4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Jul 2024 10:18:44 +0100 Subject: [PATCH 46/48] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/ui/StateBuilder.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index da536bd046..1925e60ee7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -676,6 +676,7 @@ internal class StateBuilder( return when (dataError) { is DataError.ExchangeTooSmallAmountError -> { swapProvider.convertToAvailableFromProviderState( + swapProvider = swapProvider, alertText = resourceReference( R.string.express_provider_min_amount, wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), @@ -686,6 +687,7 @@ internal class StateBuilder( } is DataError.ExchangeTooBigAmountError -> { swapProvider.convertToAvailableFromProviderState( + swapProvider = swapProvider, alertText = resourceReference( R.string.express_provider_max_amount, wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), @@ -1610,10 +1612,16 @@ internal class StateBuilder( } private fun SwapProvider.convertToAvailableFromProviderState( + swapProvider: SwapProvider, alertText: TextReference, selectionType: ProviderState.SelectionType, onProviderClick: (String) -> Unit, ): ProviderState { + val additionalBadge = if (swapProvider.isRecommended) { + ProviderState.AdditionalBadge.Recommended + } else { + ProviderState.AdditionalBadge.Empty + } return ProviderState.Content( id = this.providerId, name = this.name, @@ -1621,7 +1629,7 @@ internal class StateBuilder( type = this.type.providerName, selectionType = selectionType, subtitle = alertText, - additionalBadge = ProviderState.AdditionalBadge.Empty, + additionalBadge = additionalBadge, percentLowerThenBest = PercentDifference.Empty, namePrefix = ProviderState.PrefixType.NONE, onProviderClick = onProviderClick, From 8fbfee735b9e171771170890cabf6a009cb73020 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Jul 2024 22:39:27 +0400 Subject: [PATCH 47/48] Updated on 2026-08-14 --- .../tangem/tap/ForegroundActivityObserver.kt | 10 +-- .../tap/data}/DefaultCardSdkProvider.kt | 67 ++++++++++++++++--- .../tangem/tap/data/TangemSdkWarningEvent.kt | 9 +++ .../com/tangem/tap/di/data}/CardSdkModule.kt | 4 +- .../tangem/data/card/sdk/CardSdkProvider.kt | 2 +- 5 files changed, 73 insertions(+), 19 deletions(-) rename {data/card/src/main/java/com/tangem/data/card/sdk => app/src/main/java/com/tangem/tap/data}/DefaultCardSdkProvider.kt (63%) create mode 100644 app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt rename {data/card/src/main/java/com/tangem/data/card/di => app/src/main/java/com/tangem/tap/di/data}/CardSdkModule.kt (86%) diff --git a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt index ecfb707001..f459c74e77 100644 --- a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt +++ b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt @@ -7,16 +7,16 @@ import android.os.Bundle import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity -import java.util.* +import java.util.WeakHashMap import kotlin.reflect.KClass class ForegroundActivityObserver : ActivityResultCaller { override var activityResultLauncher: ActivityResultLauncher? = null private set - private val activities = WeakHashMap, Activity>() + private val activities = WeakHashMap, AppCompatActivity>() - val foregroundActivity: Activity? + val foregroundActivity: AppCompatActivity? get() = activities.entries .filterNot { it.value.isDestroyed } .firstOrNull() @@ -35,7 +35,7 @@ class ForegroundActivityObserver : ActivityResultCaller { } override fun onActivityResumed(activity: Activity) { - activities[activity::class] = activity + activities[activity::class] = activity as? AppCompatActivity } override fun onActivityDestroyed(activity: Activity) { @@ -59,6 +59,6 @@ class ForegroundActivityObserver : ActivityResultCaller { } } -fun ForegroundActivityObserver.withForegroundActivity(block: (Activity) -> Unit) { +fun ForegroundActivityObserver.withForegroundActivity(block: (AppCompatActivity) -> Unit) { foregroundActivity?.let { block(it) } } \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt similarity index 63% rename from data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt rename to app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt index cdc3c2e305..a164e0dde1 100644 --- a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt @@ -1,8 +1,7 @@ -package com.tangem.data.card.sdk +package com.tangem.tap.data import androidx.fragment.app.FragmentActivity import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner import com.tangem.Log import com.tangem.TangemSdk @@ -11,11 +10,15 @@ import com.tangem.common.authentication.AuthenticationManager import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.Config import com.tangem.common.services.secure.SecureStorage +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.crypto.bip39.Wordlist +import com.tangem.data.card.sdk.CardSdkOwner +import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.sdk.DefaultSessionViewDelegate import com.tangem.sdk.extensions.* import com.tangem.sdk.nfc.NfcManager import com.tangem.sdk.storage.create +import com.tangem.tap.foregroundActivityObserver import javax.inject.Inject import javax.inject.Singleton @@ -25,19 +28,24 @@ import javax.inject.Singleton [REDACTED_AUTHOR] */ @Singleton -internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, CardSdkOwner { +internal class DefaultCardSdkProvider @Inject constructor( + private val analyticsEventHandler: AnalyticsEventHandler, +) : CardSdkProvider, CardSdkOwner { - override val sdk: TangemSdk - get() = requireNotNull(value = holder?.sdk) { - "Impossible to get the TangemSdk when activity is destroyed" - } - - private val observer: LifecycleObserver = Observer() + private val observer = Observer() private var holder: Holder? = null + override val sdk: TangemSdk + get() = holder?.sdk ?: tryToRegisterWithForegroundActivity() + override fun register(activity: FragmentActivity) { - Log.info { "Tangem SDK owner registered" } + if (activity.isDestroyed || activity.isFinishing || activity.isChangingConfigurations) { + val message = "Tangem SDK owner registration skipped: activity is destroyed or finishing" + analyticsEventHandler.send(TangemSdkWarningEvent(message)) + Log.info { message } + return + } if (holder != null) { unsubscribeAndCleanup() @@ -46,6 +54,36 @@ internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, C initialize(activity) activity.lifecycle.addObserver(observer) + + Log.info { "Tangem SDK owner registered" } + } + + private fun tryToRegisterWithForegroundActivity(): TangemSdk { + val warning = "Tangem SDK holder is null, trying to recreate it with foreground activity" + analyticsEventHandler.send(TangemSdkWarningEvent(warning)) + Log.warning { warning } + + val activity = foregroundActivityObserver.foregroundActivity + + if (activity == null) { + val error = "Tangem SDK holder is null and foreground activity is null" + analyticsEventHandler.send(TangemSdkWarningEvent(error)) + Log.error { error } + error(error) + } + + register(activity) + + val sdk = holder?.sdk + + if (sdk == null) { + val error = "Tangem SDK is null after re-registering with foreground activity" + analyticsEventHandler.send(TangemSdkWarningEvent(error)) + Log.error { error } + error(error) + } + + return sdk } private fun initialize(activity: FragmentActivity) { @@ -78,7 +116,14 @@ internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, C } private fun unsubscribeAndCleanup() { - with(receiver = holder ?: return) { + val currentHolder = holder + + if (currentHolder == null) { + Log.info { "Tangem SDK already unsubscribed and cleaned up" } + return + } + + with(currentHolder) { nfcManager.unsubscribe(activity) authenticationManager.unsubscribe(activity) diff --git a/app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt b/app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt new file mode 100644 index 0000000000..1393bfbf57 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt @@ -0,0 +1,9 @@ +package com.tangem.tap.data + +import com.tangem.core.analytics.models.AnalyticsEvent + +internal class TangemSdkWarningEvent(message: String) : AnalyticsEvent( + category = "Tangem SDK", + event = "Warning", + error = IllegalStateException(message), +) \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt b/app/src/main/java/com/tangem/tap/di/data/CardSdkModule.kt similarity index 86% rename from data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt rename to app/src/main/java/com/tangem/tap/di/data/CardSdkModule.kt index 891c418cb1..118bcb16a7 100644 --- a/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/CardSdkModule.kt @@ -1,8 +1,8 @@ -package com.tangem.data.card.di +package com.tangem.tap.di.data import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.data.card.sdk.CardSdkProvider -import com.tangem.data.card.sdk.DefaultCardSdkProvider +import com.tangem.tap.data.DefaultCardSdkProvider import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt index 0d0b88fa41..71c34a1266 100644 --- a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt +++ b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt @@ -7,7 +7,7 @@ import com.tangem.TangemSdk * [REDACTED_AUTHOR] */ -internal interface CardSdkProvider { +interface CardSdkProvider { /** CardSDK instance */ val sdk: TangemSdk From 41b5b570e54ee6ba6550dc6add8d4d9f2c45fe5d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jul 2024 10:59:08 +0200 Subject: [PATCH 48/48] Updated on 2026-08-14 --- .../tangem/data/promo/DefaultPromoRepository.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index 94a184fb73..e48f0e6933 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -33,17 +33,19 @@ internal class DefaultPromoRepository( } override suspend fun getOkxPromoBanner(): PromoBanner? { - return runCatching(dispatchers.io) { - promoResponseConverter.convert( - tangemApi.getPromotionInfo(OKX) - .getOrThrow(), - ) - }.getOrNull() + // TODO disabled for 5.12, enable for 5.12.1 + return null + // return runCatching(dispatchers.io) { + // promoResponseConverter.convert( + // tangemApi.getPromotionInfo(OKX) + // .getOrThrow(), + // ) + // }.getOrNull() } private companion object { private const val CHANGELLY_NAME = "changelly" private const val TRAVALA = "travala" - private const val OKX = "okx" + // private const val OKX = "okx" } } \ No newline at end of file