From f10002c0e574cbfe9b707d151a14e7b5b31d4c69 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Dec 2025 19:05:00 +0400 Subject: [PATCH 01/15] Updated on 2026-08-14 --- .../notifications/YieldSupplyNotificationsComponent.kt | 2 ++ .../notifications/model/YieldSupplyNotificationsModel.kt | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt index 0e5ade929b..533ecd122d 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/YieldSupplyNotificationsComponent.kt @@ -1,6 +1,7 @@ package com.tangem.features.yield.supply.impl.subcomponents.notifications import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -35,6 +36,7 @@ internal class YieldSupplyNotificationsComponent( modifier = modifier .fillMaxWidth() .animateContentSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { state.forEachIndexed { index, item -> Notification( diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt index 2e81b51d3f..58e72d7f9a 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt @@ -91,7 +91,9 @@ internal class YieldSupplyNotificationsModel @Inject constructor( uiState.update { notifications.toPersistentList() } - yieldSupplyNotificationsUpdateListener.callbackHasError(notifications.any()) + // Business requirement that YieldSupplyHighNetworkFee is not an error and doesn't block the button + val hasError = notifications.any { it !is NotificationUM.Info.YieldSupplyHighNetworkFee } + yieldSupplyNotificationsUpdateListener.callbackHasError(hasError) }.launchIn(modelScope) } From c3c4eaf1f671c70c15c3c02588f8a75d1fbdd777 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 10:02:42 +0400 Subject: [PATCH 02/15] Updated on 2026-08-14 --- .../api/analytics/YieldSupplyAnalytics.kt | 11 +++++++ .../model/YieldSupplyNotificationsModel.kt | 29 ++++++++++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt index 37b00d61b2..4b7b61f898 100644 --- a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt +++ b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt @@ -204,6 +204,17 @@ sealed class YieldSupplyAnalytics( ), ) + data class NoticeHighFee( + val token: String, + val blockchain: String, + ) : YieldSupplyAnalytics( + event = "Notice - High Network Fee", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) + enum class Action(val value: String) { Start("Start"), Approve("Approve"), diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt index 58e72d7f9a..fc8a944729 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt @@ -80,14 +80,7 @@ internal class YieldSupplyNotificationsModel @Inject constructor( } } - if (notifications.any { it is NotificationUM.Error.TokenExceedsBalance }) { - analyticsEventHandler.send( - YieldSupplyAnalytics.NoticeNotEnoughFee( - token = cryptoCurrencyStatus.currency.symbol, - blockchain = cryptoCurrencyStatus.currency.network.name, - ), - ) - } + sendAnalytics(notifications, cryptoCurrencyStatus.currency) uiState.update { notifications.toPersistentList() } @@ -97,6 +90,26 @@ internal class YieldSupplyNotificationsModel @Inject constructor( }.launchIn(modelScope) } + private fun sendAnalytics(notifications: List, currency: CryptoCurrency) { + if (notifications.any { it is NotificationUM.Error.TokenExceedsBalance }) { + analyticsEventHandler.send( + YieldSupplyAnalytics.NoticeNotEnoughFee( + token = currency.symbol, + blockchain = currency.network.name, + ), + ) + } + + if (notifications.any { it is NotificationUM.Info.YieldSupplyHighNetworkFee }) { + analyticsEventHandler.send( + YieldSupplyAnalytics.NoticeHighFee( + token = currency.symbol, + blockchain = currency.network.name, + ), + ) + } + } + private fun openTokenDetails(cryptoCurrency: CryptoCurrency) { appRouter.push( AppRoute.CurrencyDetails( From 54f0be3e6c60d50116c334590b939cfc12cffe28 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 12:34:13 +0400 Subject: [PATCH 03/15] Updated on 2026-08-14 --- .../impl/main/model/YieldSupplyModel.kt | 153 +++++++++--------- 1 file changed, 76 insertions(+), 77 deletions(-) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index c6bb211f29..25e0ab888d 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -78,6 +78,7 @@ internal class YieldSupplyModel @Inject constructor( var userWallet: UserWallet by Delegates.notNull() private val fetchCurrencyJobHolder = JobHolder() + private val loadStatusJobHolder = JobHolder() private var lastStatusCheckTimestamp = 0L private val isFirstCryptoCurrencyStatusEmission = AtomicBoolean(true) @@ -134,22 +135,20 @@ internal class YieldSupplyModel @Inject constructor( } } - private fun loadTokenStatus() { + private suspend fun loadTokenStatus() { val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return - modelScope.launch(dispatchers.default) { - yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) - .onRight { tokenStatus -> - uiState.update( - YieldSupplyTokenStatusSuccessTransformer( - tokenStatus = tokenStatus, - onStartEarningClick = ::onStartEarningClick, - ), - ) - }.onLeft { - Timber.e(it) - uiState.update { YieldSupplyUM.Initial } - } - } + yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) + .onRight { tokenStatus -> + uiState.update( + YieldSupplyTokenStatusSuccessTransformer( + tokenStatus = tokenStatus, + onStartEarningClick = ::onStartEarningClick, + ), + ) + }.onLeft { + Timber.e(it) + uiState.update { YieldSupplyUM.Initial } + } } override fun onStartEarningClick() { @@ -183,7 +182,9 @@ internal class YieldSupplyModel @Inject constructor( } @Suppress("MaximumLineLength") - private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch { + private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch( + dispatchers.default, + ) { val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus val tokenProtocolStatus = yieldSupplyRepository.getTokenProtocolStatus( userWallet.walletId, @@ -234,7 +235,7 @@ internal class YieldSupplyModel @Inject constructor( lastStatusCheckTimestamp = 0L } } - } + }.saveIn(loadStatusJobHolder) private fun showProcessing(status: YieldSupplyEnterStatus) { uiState.update { @@ -246,24 +247,21 @@ internal class YieldSupplyModel @Inject constructor( fetchCurrencyWithDelay() } - private fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) { + private suspend fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) { val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus - modelScope - .launch { - yieldSupplyRepository.saveTokenProtocolStatus( - userWalletId = userWallet.walletId, - cryptoCurrency = cryptoCurrency, - yieldSupplyEnterStatus = null, - ) - if (yieldSupplyStatus?.isActive == true) { - loadActiveState( - cryptoCurrencyStatus = cryptoCurrencyStatus, - yieldSupplyStatus = yieldSupplyStatus, - ) - } else { - loadTokenStatus() - } - } + yieldSupplyRepository.saveTokenProtocolStatus( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrency, + yieldSupplyEnterStatus = null, + ) + if (yieldSupplyStatus?.isActive == true) { + loadActiveState( + cryptoCurrencyStatus = cryptoCurrencyStatus, + yieldSupplyStatus = yieldSupplyStatus, + ) + } else { + loadTokenStatus() + } } private fun fetchCurrencyWithDelay() { @@ -280,7 +278,10 @@ internal class YieldSupplyModel @Inject constructor( }.saveIn(fetchCurrencyJobHolder) } - private fun loadActiveState(cryptoCurrencyStatus: CryptoCurrencyStatus, yieldSupplyStatus: YieldSupplyStatus) { + private suspend fun loadActiveState( + cryptoCurrencyStatus: CryptoCurrencyStatus, + yieldSupplyStatus: YieldSupplyStatus, + ) { val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return val showWarningIcon = !yieldSupplyStatus.isAllowedToSpend val isShowInfoIconPrevState = when (val state = uiState.value) { @@ -295,50 +296,48 @@ internal class YieldSupplyModel @Inject constructor( ), ) } - modelScope.launch(dispatchers.default) { - yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) - .onRight { tokenStatus -> - uiState.update { - YieldSupplyUM.Content( - title = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) + .onRight { tokenStatus -> + uiState.update { + YieldSupplyUM.Content( + title = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + ), + subtitle = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, + ), + rewardsApy = combinedReference( + resourceReference( + R.string.yield_module_token_details_earn_notification_apy, ), - subtitle = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, - ), - rewardsApy = combinedReference( - resourceReference( - R.string.yield_module_token_details_earn_notification_apy, - ), - stringReference(" ${tokenStatus.apy}%"), - ), - onClick = ::onActiveClick, - showWarningIcon = showWarningIcon, - showInfoIcon = isShowInfoIconPrevState, - apy = tokenStatus.apy.toString(), - ) - } - computeAndApplyShowInfoIcon(cryptoCurrencyStatus) - }.onLeft { t -> - Timber.e(t) - uiState.update { - YieldSupplyUM.Content( - title = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, - ), - subtitle = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, - ), - rewardsApy = TextReference.EMPTY, - onClick = ::onActiveClick, - showWarningIcon = showWarningIcon, - showInfoIcon = isShowInfoIconPrevState, - apy = "", - ) - } - computeAndApplyShowInfoIcon(cryptoCurrencyStatus) + stringReference(" ${tokenStatus.apy}%"), + ), + onClick = ::onActiveClick, + showWarningIcon = showWarningIcon, + showInfoIcon = isShowInfoIconPrevState, + apy = tokenStatus.apy.toString(), + ) } - } + computeAndApplyShowInfoIcon(cryptoCurrencyStatus) + }.onLeft { t -> + Timber.e(t) + uiState.update { + YieldSupplyUM.Content( + title = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + ), + subtitle = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, + ), + rewardsApy = TextReference.EMPTY, + onClick = ::onActiveClick, + showWarningIcon = showWarningIcon, + showInfoIcon = isShowInfoIconPrevState, + apy = "", + ) + } + computeAndApplyShowInfoIcon(cryptoCurrencyStatus) + } } private fun computeAndApplyShowInfoIcon(cryptoCurrencyStatus: CryptoCurrencyStatus) { From 4cf3d71648c6938a91c81e9aa3aa0a5933302f89 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 11:53:17 +0500 Subject: [PATCH 04/15] Updated on 2026-08-14 --- .../api/stakekit/models/response/model/NetworkTypeDTO.kt | 3 +++ .../local/token/converter/StakingNetworkTypeConverter.kt | 2 ++ .../kotlin/com/tangem/domain/models/staking/NetworkType.kt | 1 + 3 files changed, 6 insertions(+) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt index 77bfadc65c..9cb0b84276 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt @@ -77,6 +77,9 @@ enum class NetworkTypeDTO { @Json(name = "canto") CANTO, + @Json(name = "cardano") + CARDANO, + @Json(name = "chihuahua") CHIHUAHUA, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt index adc411b5e5..4990e03bbf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt @@ -33,6 +33,7 @@ object StakingNetworkTypeConverter : TwoWayConverter NetworkType.BAND_PROTOCOL NetworkTypeDTO.BITSONG -> NetworkType.BITSONG NetworkTypeDTO.CANTO -> NetworkType.CANTO + NetworkTypeDTO.CARDANO -> NetworkType.CARDANO NetworkTypeDTO.CHIHUAHUA -> NetworkType.CHIHUAHUA NetworkTypeDTO.COMDEX -> NetworkType.COMDEX NetworkTypeDTO.COREUM -> NetworkType.COREUM @@ -106,6 +107,7 @@ object StakingNetworkTypeConverter : TwoWayConverter NetworkTypeDTO.BAND_PROTOCOL NetworkType.BITSONG -> NetworkTypeDTO.BITSONG NetworkType.CANTO -> NetworkTypeDTO.CANTO + NetworkType.CARDANO -> NetworkTypeDTO.CARDANO NetworkType.CHIHUAHUA -> NetworkTypeDTO.CHIHUAHUA NetworkType.COMDEX -> NetworkTypeDTO.COMDEX NetworkType.COREUM -> NetworkTypeDTO.COREUM diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt index 3e0921c351..e26a393e18 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt @@ -28,6 +28,7 @@ enum class NetworkType { BAND_PROTOCOL, BITSONG, CANTO, + CARDANO, CHIHUAHUA, COMDEX, COREUM, From 9127624b78ca5ec71bd3b842c7b85ccf739153a1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 11:53:39 +0500 Subject: [PATCH 05/15] Updated on 2026-08-14 --- .../impl/presentation/model/StakingModel.kt | 17 ++++++++++++++--- .../state/utils/StakingPendingActionUtils.kt | 16 ++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 9e1aaa62ba..70e39f6f9b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -93,6 +93,7 @@ import com.tangem.utils.extensions.isSingleItem import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -601,10 +602,20 @@ internal class StakingModel @Inject constructor( override fun onActiveStake(activeStake: BalanceState) { val networkId = cryptoCurrencyStatus.currency.network.rawId - if (isSingleAction(networkId, activeStake)) { + val preferredValidators = yield.validators.filter { it.preferred } + val pendingActions = activeStake.pendingActions.mapNotNull { action -> + if (action.type in listOf(StakingActionType.RESTAKE, StakingActionType.STAKE) && + preferredValidators.isSingleItem() + ) { + null + } else { + action + } + }.toImmutableList() + if (isSingleAction(networkId, pendingActions)) { prepareForConfirmation( balanceType = activeStake.type, - pendingActions = activeStake.pendingActions, + pendingActions = pendingActions, balanceState = activeStake, validator = activeStake.validator, amountValue = activeStake.cryptoValue, @@ -613,7 +624,7 @@ internal class StakingModel @Inject constructor( } else { stateController.update( ShowActionSelectorBottomSheetTransformer( - pendingActions = withStubUnstakeAction(networkId, activeStake), + pendingActions = withStubUnstakeAction(networkId, pendingActions, activeStake), onActionSelect = { action -> prepareForConfirmation( balanceType = activeStake.type, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt index 6ef20b93a9..d229b97f60 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt @@ -37,17 +37,21 @@ internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (t null -> TextReference.EMPTY } -internal fun isSingleAction(networkId: String, activeStake: BalanceState): Boolean { - val isSingleAction = activeStake.pendingActions.size <= 1 // Either single or none pending actions - val isCompositePendingActions = isCompositePendingActions(networkId, activeStake.pendingActions) - val isRestake = activeStake.pendingActions.any { it.type.isRestake } +internal fun isSingleAction(networkId: String, pendingActions: List): Boolean { + val isSingleAction = pendingActions.size <= 1 // Either single or none pending actions + val isCompositePendingActions = isCompositePendingActions(networkId, pendingActions.toPersistentList()) + val isRestake = pendingActions.any { it.type.isRestake } return isSingleAction && !isRestake || isCompositePendingActions } -internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState): ImmutableList { +internal fun withStubUnstakeAction( + networkId: String, + pendingActions: List, + activeStake: BalanceState, +): ImmutableList { return if (isStubUnstakeAction(networkId) && activeStake.type != BalanceType.REWARDS) { - activeStake.pendingActions.plus( + pendingActions.plus( PendingAction( type = StakingActionType.UNSTAKE, passthrough = "", From 587a31520daa8c4ba29be45d481252c9ffadf94b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 13:35:11 +0300 Subject: [PATCH 06/15] Updated on 2026-08-14 --- .../root/RootDetectedWarningComponent.kt | 68 ++++++++++++++ .../root/RootDetectedWarningContent.kt | 93 +++++++++++++++++++ .../com/tangem/tap/routing/RootContent.kt | 3 + .../component/impl/DefaultRoutingComponent.kt | 9 ++ .../local/preferences/PreferencesKeys.kt | 2 + core/res/src/main/res/values-ru/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 2 + .../core/ui/components/DialogFullScreen.kt | 70 +++++++------- .../message/MessageBottomSheetV2.kt | 24 ++--- .../ui/components/icons/HighlightedIcon.kt | 39 ++++++++ .../settings/DefaultSettingsRepository.kt | 11 +++ .../repositories/SettingsRepository.kt | 4 + 12 files changed, 274 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt create mode 100644 app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt diff --git a/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt new file mode 100644 index 0000000000..16077bce72 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt @@ -0,0 +1,68 @@ +package com.tangem.tap.features.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.essenty.instancekeeper.getOrCreateSimple +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.components.DialogFullScreen +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.security.isSecurityExposed +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +@Suppress("UnusedPrivateProperty") +class RootDetectedWarningComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Unit, + private val securityInfoProvider: DeviceSecurityInfoProvider, + private val settingsRepository: SettingsRepository, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val isShown = instanceKeeper.getOrCreateSimple { MutableStateFlow(false) } + + suspend fun tryToShowWarningAndWaitContinuation() { + if (isShown.value) return + + if (settingsRepository.isRootDetectedWarningShown().not() && securityInfoProvider.isSecurityExposed()) { + isShown.value = true + } + + isShown.first { it == false } // Wait until the warning is dismissed + } + + @Composable + override fun Content(modifier: Modifier) { + val isShownState by isShown.collectAsStateWithLifecycle() + + if (isShownState) { + DialogFullScreen(onDismissRequest = {}) { + RootDetectedWarningContent( + modifier = modifier, + onContinueClick = remember(this) { ::onContinueClick }, + ) + } + } + } + + private fun onContinueClick() { + componentScope.launch { + settingsRepository.setRootDetectedWarningShown(true) + isShown.value = false + } + } + + @AssistedFactory + interface Factory : ComponentFactory { + override fun create(context: AppComponentContext, params: Unit): RootDetectedWarningComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt new file mode 100644 index 0000000000..9dc01d8a13 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt @@ -0,0 +1,93 @@ +package com.tangem.tap.features.root + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.icons.HighlightedIcon +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.wallet.R + +@Composable +internal fun RootDetectedWarningContent(modifier: Modifier = Modifier, onContinueClick: () -> Unit = {}) { + Column( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary) + .statusBarsPadding() + .padding(horizontal = 16.dp), + ) { + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center, + ) { + InfoBlock( + modifier = Modifier.padding(top = 48.dp, bottom = 24.dp), + ) + } + + PrimaryButton( + modifier = Modifier + .navigationBarsPadding() + .padding(bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.common_understand_continue), + onClick = onContinueClick, + ) + } +} + +@Composable +private fun InfoBlock(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + HighlightedIcon( + icon = R.drawable.ic_alert_circle_24, + iconTint = TangemTheme.colors.icon.warning, + ) + + SpacerH(20.dp) + + Text( + text = stringResourceSafe(R.string.root_detected_warning_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + SpacerH(12.dp) + + Text( + modifier = Modifier.padding(horizontal = 24.dp), + text = stringResourceSafe(R.string.root_detected_warning_description), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + RootDetectedWarningContent() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/RootContent.kt b/app/src/main/java/com/tangem/tap/routing/RootContent.kt index add9354694..c21f7d2617 100644 --- a/app/src/main/java/com/tangem/tap/routing/RootContent.kt +++ b/app/src/main/java/com/tangem/tap/routing/RootContent.kt @@ -47,6 +47,7 @@ internal fun RootContent( modifier: Modifier = Modifier, wcContent: @Composable (modifier: Modifier) -> Unit, hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit, + rootDetectedWarningContent: @Composable (modifier: Modifier) -> Unit, ) { val context = LocalContext.current @@ -82,6 +83,8 @@ internal fun RootContent( hotAccessCodeContent(Modifier.fillMaxSize()) + rootDetectedWarningContent(Modifier.fillMaxSize()) + TangemSnackbarHost( modifier = Modifier .align(Alignment.BottomCenter) diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 65ece0eaad..23bc630cec 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -38,6 +38,7 @@ import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.hot.TangemHotSDKProxy import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog +import com.tangem.tap.features.root.RootDetectedWarningComponent import com.tangem.tap.routing.RootContent import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.component.RoutingComponent.Child @@ -64,6 +65,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val tangemHotSDKProxy: TangemHotSDKProxy, private val hotAccessCodeRequestComponentFactory: HotAccessCodeRequestComponent.Factory, private val hotAccessCodeRequesterProxy: HotWalletPasswordRequesterProxy, + private val rootDetectedWarningComponentFactory: RootDetectedWarningComponent.Factory, private val userWalletsListRepository: UserWalletsListRepository, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, @@ -85,6 +87,11 @@ internal class DefaultRoutingComponent @AssistedInject constructor( .create(child("hotAccessCodeRequestComponent"), Unit) } + private val rootDetectedWarningComponent: RootDetectedWarningComponent by lazy { + rootDetectedWarningComponentFactory + .create(child("rootDetectedWarningComponent"), Unit) + } + private val navigation = navigationProvider.getOrCreateTyped() private val stack: Value> = childStack( @@ -134,6 +141,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private fun initializeInitialNavigation() { if (initialStack.isNullOrEmpty()) { componentScope.launch { + rootDetectedWarningComponent.tryToShowWarningAndWaitContinuation() val initialRoute = resolveInitialRoute() router.replaceAll(initialRoute) } @@ -177,6 +185,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( modifier = modifier, wcContent = { wcRoutingComponent.Content(it) }, hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) }, + rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) }, ) } 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 b52614222b..78e99e42bd 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 @@ -27,6 +27,8 @@ object PreferencesKeys { val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") } + val ROOT_DETECTED_WARNING_SHOWN_KEY by lazy { booleanPreferencesKey(name = "rootDetectedWarningShown") } + val SHOULD_SHOW_ASK_BIOMETRY_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") } val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") } diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 0d5eaa197f..03ce372d0f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -2017,7 +2017,7 @@ %1$s выведено из Aave Режим доходности инициализирован Режим доходности реактивирован - Перевод средств в Aave + Перевод в Aave %1$s отправлено в Aave Вывод из Aave Автоматически diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 524e2ec97f..07f6bd26c8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1091,6 +1091,8 @@ Please reset the next device to continue Ring owners get 3 commission-free swaps on Changelly until 15.11! Swap With 0% Fees Now! + Devices with root access are considered less secure. Your data may be exposed to additional risks. + Root access detected Log into the app and check your balance without scanning the card or ring Access the app Allow to use biometrics diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt index 6f70db0ba1..44e48c0fd0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt @@ -37,44 +37,46 @@ fun DialogFullScreen( decorFitsSystemWindows = false, ), content = { - val activityWindow = getActivityWindow() - val dialogWindow = getDialogWindow() - val parentView = LocalView.current.parent as View - SideEffect { - if (activityWindow != null && dialogWindow != null) { - val attributes = WindowManager.LayoutParams().apply { - copyFrom(activityWindow.attributes) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE - } else { - flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS - } - type = dialogWindow.attributes.type - } - - dialogWindow.attributes = attributes - parentView.layoutParams = - FrameLayout.LayoutParams( - activityWindow.decorView.width, - activityWindow.decorView.height, - ) - } - } - - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - val systemUiController = rememberSystemUiController(getActivityWindow()) - val dialogSystemUiController = rememberSystemUiController(getDialogWindow()) - + ProvideSystemBarsIconsController { + val activityWindow = getActivityWindow() + val dialogWindow = getDialogWindow() + val parentView = LocalView.current.parent as View SideEffect { - systemUiController.setSystemBarsColor(color = Color.Transparent) - dialogSystemUiController.setSystemBarsColor(color = Color.Transparent) + if (activityWindow != null && dialogWindow != null) { + val attributes = WindowManager.LayoutParams().apply { + copyFrom(activityWindow.attributes) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + } else { + flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + } + type = dialogWindow.attributes.type + } + + dialogWindow.attributes = attributes + parentView.layoutParams = + FrameLayout.LayoutParams( + activityWindow.decorView.width, + activityWindow.decorView.height, + ) + } } - } - SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not()) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + val systemUiController = rememberSystemUiController(getActivityWindow()) + val dialogSystemUiController = rememberSystemUiController(getDialogWindow()) - Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) { - content() + SideEffect { + systemUiController.setSystemBarsColor(color = Color.Transparent) + dialogSystemUiController.setSystemBarsColor(color = Color.Transparent) + } + } + + SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not()) + + Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) { + content() + } } }, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt index f31c547290..cb3f2c538d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt @@ -2,17 +2,13 @@ package com.tangem.core.ui.components.bottomsheets.message import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -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.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -25,6 +21,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.components.icons.HighlightedIcon import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -144,20 +141,11 @@ private fun BottomSheetIcon(icon: MessageBottomSheetUMV2.Icon, modifier: Modifie MessageBottomSheetUMV2.Icon.BackgroundType.Warning -> TangemTheme.colors.icon.warning } - Box( - modifier = modifier - .size(TangemTheme.dimens.size56) - .clip(CircleShape) - .background(backgroundColor.copy(alpha = 0.1F)), - contentAlignment = Alignment.Center, - content = { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size32), - painter = painterResource(icon.res), - contentDescription = null, - tint = tint, - ) - }, + HighlightedIcon( + modifier = modifier, + icon = icon.res, + iconTint = tint, + backgroundColor = backgroundColor, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt new file mode 100644 index 0000000000..a94578a5ff --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt @@ -0,0 +1,39 @@ +package com.tangem.core.ui.components.icons + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +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.res.painterResource +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun HighlightedIcon( + @DrawableRes icon: Int, + iconTint: Color, + modifier: Modifier = Modifier, + backgroundColor: Color = iconTint, +) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size56) + .clip(CircleShape) + .background(backgroundColor.copy(alpha = 0.1F)), + contentAlignment = Alignment.Center, + content = { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size32), + painter = painterResource(icon), + contentDescription = null, + tint = iconTint, + ) + }, + ) +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index dea79f4d3a..588d3b71e3 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -193,4 +193,15 @@ internal class DefaultSettingsRepository( default = false, ) } + + override suspend fun isRootDetectedWarningShown(): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.ROOT_DETECTED_WARNING_SHOWN_KEY, + default = false, + ) + } + + override suspend fun setRootDetectedWarningShown(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.ROOT_DETECTED_WARNING_SHOWN_KEY, value = value) + } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 0a8f854790..68ffcaf957 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -56,4 +56,8 @@ interface SettingsRepository { suspend fun setGooglePayAvailability(value: Boolean) suspend fun isGooglePayAvailability(): Boolean + + suspend fun isRootDetectedWarningShown(): Boolean + + suspend fun setRootDetectedWarningShown(value: Boolean) } \ No newline at end of file From a6598eccc8789bfade92ca906367b4aaa22d253c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 15:41:05 +0500 Subject: [PATCH 07/15] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 17 ++--- .../ui/tokens/TokenItemStateConverter.kt | 70 +++++++------------ .../tangem/utils/coroutines/CoroutineExt.kt | 21 ++++++ .../staking/usecase/StakingApyFlowUseCase.kt | 36 ---------- .../usecase/StakingAvailabilityListUseCase.kt | 37 ++++++++++ .../implementors/MultiWalletContentLoader.kt | 6 +- .../MultiWalletContentLoaderFactory.kt | 6 +- .../SingleWalletWithTokenContentLoader.kt | 6 +- ...ngleWalletWithTokenContentLoaderFactory.kt | 6 +- .../transformers/SetTokenListTransformer.kt | 7 +- .../converter/TokenListStateConverter.kt | 7 +- .../subscribers/AccountListSubscriber.kt | 33 +++++---- .../subscribers/BasicAccountListSubscriber.kt | 17 ++--- .../subscribers/BasicTokenListSubscriber.kt | 24 +++---- .../MultiWalletTokenListSubscriber.kt | 6 +- .../SingleWalletWithTokenListSubscriber.kt | 6 +- 16 files changed, 156 insertions(+), 149 deletions(-) delete mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingApyFlowUseCase.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingAvailabilityListUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 631f9a46a0..aea1890763 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -1,15 +1,9 @@ package com.tangem.tap.di.domain import com.tangem.domain.staking.* -import com.tangem.domain.staking.repositories.P2PEthPoolRepository -import com.tangem.domain.staking.repositories.StakeKitActionRepository -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakeKitRepository -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.staking.repositories.StakeKitTransactionHashRepository +import com.tangem.domain.staking.repositories.* import com.tangem.domain.staking.single.SingleYieldBalanceFetcher -import com.tangem.domain.staking.toggles.StakingFeatureToggles -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides @@ -238,10 +232,7 @@ internal object StakingDomainModule { @Provides @Singleton - fun provideStakingApyFlowUseCase( - stakeKitRepository: StakeKitRepository, - stakingFeatureToggles: StakingFeatureToggles, - ): StakingApyFlowUseCase { - return StakingApyFlowUseCase(stakeKitRepository, stakingFeatureToggles) + fun provideStakingApyFlowUseCase(stakingRepository: StakingRepository): StakingAvailabilityListUseCase { + return StakingAvailabilityListUseCase(stakingRepository) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index ce46ff400f..029babd400 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -1,6 +1,5 @@ package com.tangem.common.ui.tokens -import com.tangem.blockchain.common.Blockchain import com.tangem.common.ui.R import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter @@ -22,7 +21,8 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.yieldSupplyKey import com.tangem.domain.models.staking.YieldBalance -import com.tangem.domain.staking.model.isStakingSupported +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingOption import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.lib.crypto.BlockchainUtils @@ -44,7 +44,7 @@ import java.math.BigDecimal class TokenItemStateConverter( private val appCurrency: AppCurrency, private val yieldModuleApyMap: Map = emptyMap(), - private val stakingApyMap: Map> = emptyMap(), + private val stakingApyMap: Map = emptyMap(), private val yieldSupplyPromoBannerKey: String? = null, private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = { CryptoCurrencyToIconStateConverter().convert(it) @@ -177,7 +177,7 @@ class TokenItemStateConverter( private fun createTitleState( currencyStatus: CryptoCurrencyStatus, yieldModuleApyMap: Map, - stakingApyMap: Map>, + stakingApyMap: Map, onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)?, ): TokenItemState.TitleState { return when (val value = currencyStatus.value) { @@ -217,7 +217,7 @@ class TokenItemStateConverter( private fun resolveEarnApy( cryptoCurrencyStatus: CryptoCurrencyStatus, yieldModuleApyMap: Map, - stakingApyMap: Map>, + stakingApyMap: Map, ): EarnApyInfo? { val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token if (token != null && yieldModuleApyMap.isNotEmpty()) { @@ -272,46 +272,42 @@ class TokenItemStateConverter( private fun findStakingRate( currencyStatus: CryptoCurrencyStatus, - stakingApyMap: Map>, + stakingApyMap: Map, ): StakingLocalInfo { - val stakingKey = currencyStatus.currency.stakingKey() - val validators = stakingApyMap[stakingKey] + val stakingAvailability = stakingApyMap[currencyStatus.currency] as? StakingAvailability.Available ?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null) val yieldBalance = currencyStatus.value.yieldBalance val hasStakedBalance = yieldBalance is YieldBalance.Data - val rateInfo: Pair? = if (hasStakedBalance) { - val validatorsByAddress = validators.associateBy { it.address } - yieldBalance.balance.items - .mapNotNull { it.validatorAddress } - .firstNotNullOfOrNull { address -> - val validator = validatorsByAddress[address] - validator?.rewardInfo?.rate?.let { rate -> - rate to validator.rewardInfo?.type - } - } - ?: validators + val rateInfo = when (val stakingOptions = stakingAvailability.option) { + is StakingOption.P2P -> null // todo p2p + is StakingOption.StakeKit -> if (hasStakedBalance) { + val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address } + yieldBalance.balance.items + .mapNotNull { it.validatorAddress } + .firstNotNullOfOrNull { address -> + validatorsByAddress[address]?.rewardInfo + } ?: stakingOptions.yield.validators .filter { it.preferred } .mapNotNull { validator -> - validator.rewardInfo?.rate?.let { rate -> rate to validator.rewardInfo?.type } + validator.rewardInfo } - .maxByOrNull { it.first } - } else { - validators - .filter { it.preferred } - .mapNotNull { validator -> - validator.rewardInfo?.rate?.let { rate -> - rate to validator.rewardInfo?.type + .maxByOrNull { it.rate } + } else { + stakingOptions.yield.validators + .filter { it.preferred } + .mapNotNull { validator -> + validator.rewardInfo } - } - .maxByOrNull { it.first } + .maxByOrNull { it.rate } + } } return StakingLocalInfo( - rate = rateInfo?.first, + rate = rateInfo?.rate, isActive = hasStakedBalance, - rewardType = rateInfo?.second, + rewardType = rateInfo?.type, ) } @@ -450,18 +446,6 @@ class TokenItemStateConverter( } fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = sources.total == StatusSource.CACHE - - private fun CryptoCurrency.stakingKey(): String { - if (this is CryptoCurrency.Coin && !network.isStakingSupported) return "" - - if (network.isStakingSupported && this !is CryptoCurrency.Coin) { - val isPolygonTokenOnEthereum = this is CryptoCurrency.Token && - this.network.id.rawId.value == Blockchain.Ethereum.id && - this.symbol == Blockchain.Polygon.currency - if (!isPolygonTokenOnEthereum) return "" - } - return "${id.rawCurrencyId}_$symbol" - } } private data class StakingLocalInfo( diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt index 7d14d15efb..1a52fae358 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt @@ -68,6 +68,27 @@ fun CoroutineScope.launchOnCancellation(block: suspend () -> Unit) { } } +@Suppress("LongParameterList", "MagicNumber") +inline fun combine6( + flow1: Flow, + flow2: Flow, + flow3: Flow, + flow4: Flow, + flow5: Flow, + flow6: Flow, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R, +): Flow = combine(flow1, flow2, flow3, flow4, flow5, flow6) { arr -> + @Suppress("UNCHECKED_CAST") + transform( + arr[0] as T1, + arr[1] as T2, + arr[2] as T3, + arr[3] as T4, + arr[4] as T5, + arr[5] as T6, + ) +} + @Suppress("LongParameterList", "MagicNumber") inline fun combine7( flow1: Flow, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingApyFlowUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingApyFlowUseCase.kt deleted file mode 100644 index 22f83164b2..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingApyFlowUseCase.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.domain.staking.usecase - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.toCoinId -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.repositories.StakeKitRepository -import com.tangem.domain.staking.toggles.StakingFeatureToggles -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -/** - * Emits a map of Validators values per currency for staking. - * - * Return map: - * - key: currency staking key (network.backendId + "_" + symbol) - * - value: validators - */ -class StakingApyFlowUseCase( - private val stakeKitRepository: StakeKitRepository, - private val stakingFeatureToggles: StakingFeatureToggles, -) { - - operator fun invoke(): Flow>> { - return stakeKitRepository.getEnabledYields() - .map { yields -> - yields.filterNot { yield -> - val isCardanoYield = yield.token.coinGeckoId == Blockchain.Cardano.toCoinId() - isCardanoYield && !stakingFeatureToggles.isCardanoStakingEnabled - }.associate { yield -> - val key = "${yield.token.coinGeckoId}_${yield.token.symbol}" - val apy = yield.validators - key to apy - } - } - } -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingAvailabilityListUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingAvailabilityListUseCase.kt new file mode 100644 index 0000000000..e38ee7233c --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/usecase/StakingAvailabilityListUseCase.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.staking.usecase + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.repositories.StakingRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope + +/** + * Returns staking availability for a list of crypto currencies for a specific user wallet + * + * Return map: + * - key: crypto currency + * - value: staking availability for the currency + */ +class StakingAvailabilityListUseCase( + private val stakingRepository: StakingRepository, +) { + + suspend fun invokeSync( + userWalletId: UserWalletId, + cryptoCurrencyList: List, + ): Map { + return coroutineScope { + cryptoCurrencyList.map { cryptoCurrency -> + async { + cryptoCurrency to stakingRepository.getStakingAvailabilitySync( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ) + } + }.awaitAll().toMap() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index e08e469afc..020e48bbb2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -5,7 +5,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository @@ -45,7 +45,7 @@ internal class MultiWalletContentLoader( private val walletsRepository: WalletsRepository, private val currenciesRepository: CurrenciesRepository, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, @@ -64,7 +64,7 @@ internal class MultiWalletContentLoader( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, applyTokenListSortingUseCase = applyTokenListSortingUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingApyFlowUseCase = stakingApyFlowUseCase, + stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ).let(::add) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 1f9d0570e8..083c1cdd5b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -5,7 +5,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository @@ -44,7 +44,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val currenciesRepository: CurrenciesRepository, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, @@ -70,7 +70,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( getNFTCollectionsUseCase = getNFTCollectionsUseCase, currenciesRepository = currenciesRepository, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingApyFlowUseCase = stakingApyFlowUseCase, + stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, hotWalletFeatureToggles = hotWalletFeatureToggles, tangemPayFeatureToggles = tangemPayFeatureToggles, tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index 6d895697ed..b5c79b14b9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -3,7 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase @@ -34,7 +34,7 @@ internal class SingleWalletWithTokenContentLoader( private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : WalletContentLoader(id = userWallet.walletId) { @@ -50,7 +50,7 @@ internal class SingleWalletWithTokenContentLoader( tokenListStore = tokenListStore, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingApyFlowUseCase = stakingApyFlowUseCase, + stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ).let(::add) MultiWalletWarningsSubscriber( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index 4d89203c74..bcf4c535a4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -4,7 +4,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase @@ -35,7 +35,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) { @@ -55,7 +55,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( getStoryContentUseCase = getStoryContentUseCase, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingApyFlowUseCase = stakingApyFlowUseCase, + stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, hotWalletFeatureToggles = hotWalletFeatureToggles, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 0dec24b4b9..39aca4fd2f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -1,8 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.StakingAvailability import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState @@ -19,7 +20,7 @@ internal class SetTokenListTransformer( private val appCurrency: AppCurrency, private val clickIntents: WalletClickIntents, private val yieldSupplyApyMap: Map = emptyMap(), - private val stakingApyMap: Map> = emptyMap(), + private val stakingAvailabilityMap: Map = emptyMap(), private val shouldShowMainPromo: Boolean, ) : WalletStateTransformer(userWallet.walletId) { @@ -64,7 +65,7 @@ internal class SetTokenListTransformer( appCurrency = appCurrency, clickIntents = clickIntents, yieldModuleApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, ).convert(value = this) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 257e68fc6b..2520491085 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -14,11 +14,12 @@ import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.StakingAvailability import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState @@ -38,7 +39,7 @@ internal class TokenListStateConverter( private val selectedWallet: UserWallet, private val clickIntents: WalletClickIntents, private val yieldModuleApyMap: Map, - private val stakingApyMap: Map>, + private val stakingAvailabilityMap: Map, private val shouldShowMainPromo: Boolean, ) : Converter { @@ -72,7 +73,7 @@ internal class TokenListStateConverter( appCurrency = appCurrency, yieldModuleApyMap = yieldModuleApyMap, yieldSupplyPromoBannerKey = yieldSupplyPromoBannerKeyConverter.convert(params), - stakingApyMap = stakingApyMap, + stakingApyMap = stakingAvailabilityMap, onItemClick = { _, status -> onTokenClick(accountId, status) }, onItemLongClick = { _, status -> onTokenLongClick(accountId, status) }, onApyLabelClick = { status, apySource, apy -> onApyLabelClick(status, apySource, apy) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt index d1cbb38064..5fb22bdc2b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt @@ -1,15 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.utils.coroutines.combine7 +import com.tangem.utils.coroutines.combine6 import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -31,29 +31,36 @@ internal class AccountListSubscriber @AssistedInject constructor( override val stateController: WalletStateController, override val clickIntents: WalletClickIntents, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicAccountListSubscriber() { - override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7( + override fun create(coroutineScope: CoroutineScope): Flow<*> = combine6( flow1 = getAccountStatusListFlow(), flow2 = getAppCurrencyFlow(), flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet), flow4 = accountDependencies.isAccountsModeEnabledUseCase(), flow5 = yieldSupplyApyFlow(), - flow6 = stakingApyFlow(), - flow7 = yieldSupplyGetShouldShowMainPromoFlow(), - transform = ::updateState, - ) + flow6 = yieldSupplyGetShouldShowMainPromoFlow(), + ) { accountList, appCurrency, expandedAccounts, isAccountMode, yieldSupplyApyMap, shouldShowMainPromo -> + updateState( + accountList = accountList, + appCurrency = appCurrency, + expandedAccounts = expandedAccounts, + isAccountMode = isAccountMode, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityListUseCase.invokeSync( + userWalletId = userWallet.walletId, + cryptoCurrencyList = accountList.flattenCurrencies().map(CryptoCurrencyStatus::currency), + ), + shouldShowMainPromo = shouldShowMainPromo, + ) + } private fun yieldSupplyApyFlow(): Flow> { return yieldSupplyApyFlowUseCase().distinctUntilChanged() } - private fun stakingApyFlow(): Flow>> { - return stakingApyFlowUseCase().distinctUntilChanged() - } - private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow { return yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index 080766b9b3..fa02716faf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -8,8 +8,9 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.error.TokenListError import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.account.AccountDependencies @@ -48,7 +49,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { expandedAccounts: Set, isAccountMode: Boolean, yieldSupplyApyMap: Map = emptyMap(), - stakingApyMap: Map> = emptyMap(), + stakingAvailabilityMap: Map = emptyMap(), shouldShowMainPromo: Boolean = false, ) { val accountFlattenCurrencies = accountList.flattenCurrencies() @@ -68,7 +69,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { appCurrency = appCurrency, portfolioId = PortfolioId(mainAccount.accountId), yieldSupplyApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, ) } @@ -89,7 +90,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { params = convertParams, appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, ) } @@ -102,7 +103,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { appCurrency: AppCurrency, portfolioId: PortfolioId, yieldSupplyApyMap: Map = emptyMap(), - stakingApyMap: Map> = emptyMap(), + stakingAvailabilityMap: Map = emptyMap(), shouldShowMainPromo: Boolean, ) { val tokenList = maybeTokenList.getOrElse( @@ -132,7 +133,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { params = TokenConverterParams.Wallet(portfolioId, tokenList), appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, ) } @@ -141,7 +142,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { params: TokenConverterParams, appCurrency: AppCurrency, yieldSupplyApyMap: Map = emptyMap(), - stakingApyMap: Map> = emptyMap(), + stakingAvailabilityMap: Map = emptyMap(), shouldShowMainPromo: Boolean, ) { stateController.update( @@ -151,7 +152,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { appCurrency = appCurrency, clickIntents = clickIntents, yieldSupplyApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, ), ) 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 36de78ef22..3fbfb3592d 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 @@ -8,11 +8,12 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase @@ -41,7 +42,7 @@ internal abstract class BasicTokenListSubscriber( private val walletWithFundsChecker: WalletWithFundsChecker, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingApyFlowUseCase: StakingApyFlowUseCase, + private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : WalletSubscriber() { @@ -71,9 +72,8 @@ internal abstract class BasicTokenListSubscriber( }, flow2 = appCurrencyFlow(), flow3 = yieldSupplyApyFlow(), - flow4 = stakingApyFlow(), - flow5 = yieldSupplyGetShouldShowMainPromoFlow(), - transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap, shouldShowMainPromo -> + flow4 = yieldSupplyGetShouldShowMainPromoFlow(), + transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, shouldShowMainPromo -> val tokenList = maybeTokenList.getOrElse( ifLoading = { maybeContent -> val isRefreshing = stateHolder.getWalletState(userWallet.walletId) @@ -101,7 +101,10 @@ internal abstract class BasicTokenListSubscriber( params = TokenConverterParams.Wallet(PortfolioId(userWallet.walletId), tokenList), appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityListUseCase.invokeSync( + userWalletId = userWallet.walletId, + cryptoCurrencyList = tokenList.flattenCurrencies().map(CryptoCurrencyStatus::currency), + ), shouldShowMainPromo = shouldShowMainPromo, ) @@ -128,7 +131,7 @@ internal abstract class BasicTokenListSubscriber( params: TokenConverterParams, appCurrency: AppCurrency, yieldSupplyApyMap: Map, - stakingApyMap: Map>, + stakingAvailabilityMap: Map, shouldShowMainPromo: Boolean, ) { stateHolder.update( @@ -138,7 +141,7 @@ internal abstract class BasicTokenListSubscriber( appCurrency = appCurrency, clickIntents = clickIntents, yieldSupplyApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, ), ) @@ -156,9 +159,6 @@ internal abstract class BasicTokenListSubscriber( private fun yieldSupplyApyFlow(): Flow> = yieldSupplyApyFlowUseCase() .distinctUntilChanged() - private fun stakingApyFlow(): Flow>> = stakingApyFlowUseCase() - .distinctUntilChanged() - private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow = yieldSupplyGetShouldShowMainPromoUseCase() .distinctUntilChanged() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index d75d637dad..c3159ba551 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -8,7 +8,7 @@ import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase @@ -32,7 +32,7 @@ internal class MultiWalletTokenListSubscriber( walletWithFundsChecker: WalletWithFundsChecker, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - stakingApyFlowUseCase: StakingApyFlowUseCase, + stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicTokenListSubscriber( userWallet = userWallet, @@ -42,7 +42,7 @@ internal class MultiWalletTokenListSubscriber( walletWithFundsChecker = walletWithFundsChecker, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingApyFlowUseCase = stakingApyFlowUseCase, + stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index 9e148dcf24..af21e47dc4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -5,7 +5,7 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.usecase.StakingApyFlowUseCase +import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase @@ -27,7 +27,7 @@ internal class SingleWalletWithTokenListSubscriber( walletWithFundsChecker: WalletWithFundsChecker, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - stakingApyFlowUseCase: StakingApyFlowUseCase, + stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : BasicTokenListSubscriber( userWallet = userWallet, @@ -37,7 +37,7 @@ internal class SingleWalletWithTokenListSubscriber( walletWithFundsChecker = walletWithFundsChecker, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingApyFlowUseCase = stakingApyFlowUseCase, + stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) { From 85a2344f628bb90245cd3764f8e009f2b19be9fc Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 12:12:52 +0400 Subject: [PATCH 08/15] Updated on 2026-08-14 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b5b03c3345..6f915e67ee 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -275,8 +275,8 @@ dependencies { debugImplementation(projects.features.kyc.impl) internalImplementation(projects.features.kyc.impl) mockedImplementation(projects.features.kyc.impl) - releaseImplementation(projects.features.kyc.mock) - externalImplementation(projects.features.kyc.mock) + releaseImplementation(projects.features.kyc.impl) + externalImplementation(projects.features.kyc.impl) implementation(projects.features.welcome.api) implementation(projects.features.welcome.impl) implementation(projects.features.createWalletSelection.api) From 92523aa0dab87ec1348f7976db2ad3f6c0794845 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 14:53:01 +0400 Subject: [PATCH 09/15] Updated on 2026-08-14 --- .../tangem/common/ui/notifications/NotificationsFactory.kt | 7 +++++++ .../notifications/model/YieldSupplyNotificationsModel.kt | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index 997da53ad1..668943b4c7 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -108,6 +108,13 @@ object NotificationsFactory { } } + // Must be called last – shown only when no other notifications exist + fun MutableList.addHighFeeNotificationIfNoOther(shouldShowHighFeeNotification: Boolean) { + if (shouldShowHighFeeNotification && this.isEmpty()) { + add(NotificationUM.Info.YieldSupplyHighNetworkFee) + } + } + fun MutableList.addReserveAmountErrorNotification( reserveAmount: BigDecimal?, sendingAmount: BigDecimal, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt index fc8a944729..9dc753015c 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt @@ -5,6 +5,7 @@ import com.tangem.common.routing.AppRouter import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addHighFeeNotificationIfNoOther import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -75,9 +76,9 @@ internal class YieldSupplyNotificationsModel @Inject constructor( onReload = params.callback::onFeeReload, ) - if (data.shouldShowHighFeeNotification) { - add(NotificationUM.Info.YieldSupplyHighNetworkFee) - } + addHighFeeNotificationIfNoOther( + shouldShowHighFeeNotification = data.shouldShowHighFeeNotification, + ) } sendAnalytics(notifications, cryptoCurrencyStatus.currency) From ec59339d51d91ed8a64fd9e233a0fe51a5993189 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 17:52:15 +0300 Subject: [PATCH 10/15] Updated on 2026-08-14 --- .../com/tangem/tap/features/welcome/model/WelcomeModel.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt index 9ca6d43223..9664e2bb7f 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.welcome.model import com.tangem.common.core.TangemError import com.tangem.common.routing.entity.InitScreenLaunchMode -import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -28,6 +28,7 @@ import javax.inject.Inject internal class WelcomeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val appFinisher: AppFinisher, + private val analyticsEventsHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model(), StoreSubscriber { @@ -44,6 +45,7 @@ internal class WelcomeModel @Inject constructor( init { subscribeToStoreChanges() initGlobalState() + analyticsEventsHandler.send(SignIn.ScreenOpened()) val welcomeAction = when (params.launchMode) { is InitScreenLaunchMode.WithCardScan -> WelcomeAction.ProceedWithCard @@ -54,12 +56,12 @@ internal class WelcomeModel @Inject constructor( } private fun unlockWallets() { - Analytics.send(SignIn.ButtonBiometricSignIn()) + analyticsEventsHandler.send(SignIn.ButtonBiometricSignIn()) store.dispatch(WelcomeAction.ProceedWithBiometrics) } private fun scanCard() { - Analytics.send(SignIn.ButtonCardSignIn()) + analyticsEventsHandler.send(SignIn.ButtonCardSignIn()) store.dispatch(WelcomeAction.ProceedWithCard) } From 9be5646b7826aa45af346abe26b6d7bd054c0b0d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 20:02:54 +0500 Subject: [PATCH 11/15] Updated on 2026-08-14 --- .../pay/models/response/BalanceResponse.kt | 6 +-- .../repository/DefaultOnboardingRepository.kt | 8 ++-- .../DefaultTangemPayCardDetailsRepository.kt | 44 ++++++++++++------- .../tangempay/entity/TangemPayDetailsUM.kt | 1 - .../transformers/DetailsBalanceTransformer.kt | 8 ---- .../tangempay/ui/TangemPayDetailsScreen.kt | 37 ---------------- 6 files changed, 36 insertions(+), 68 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/BalanceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/BalanceResponse.kt index 935c7f23e2..caccad1737 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/BalanceResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/BalanceResponse.kt @@ -6,9 +6,9 @@ import java.math.BigDecimal @JsonClass(generateAdapter = true) data class BalanceResponse( - @Json(name = "fiat") val fiat: FiatBalance, - @Json(name = "crypto") val crypto: CryptoBalance, - @Json(name = "available_for_withdrawal") val availableForWithdrawal: AvailableForWithdrawal, + @Json(name = "fiat") val fiat: FiatBalance?, + @Json(name = "crypto") val crypto: CryptoBalance?, + @Json(name = "available_for_withdrawal") val availableForWithdrawal: AvailableForWithdrawal?, ) @JsonClass(generateAdapter = true) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 563b3ee0eb..f3a791fb06 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -139,13 +139,13 @@ internal class DefaultOnboardingRepository @Inject constructor( response: CustomerMeResponse.Result?, ): CustomerInfo { val card = response?.card - val balance = response?.balance + val fiatBalance = response?.balance?.fiat val paymentAccount = response?.paymentAccount - val cardInfo = if (paymentAccount != null && card != null && balance != null) { + val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null) { CardInfo( lastFourDigits = card.cardNumberEnd, - balance = balance.fiat.availableBalance, - currencyCode = balance.fiat.currency, + balance = fiatBalance.availableBalance, + currencyCode = fiatBalance.currency, customerWalletAddress = paymentAccount.customerWalletAddress, depositAddress = response.depositAddress, ) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 1dafcfd572..b39bff96f8 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -52,20 +52,29 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( private val storePollingMutex = Mutex() override suspend fun getCardBalance(userWalletId: UserWalletId): Either { - return requestHelper.runWithErrorLogs(TAG) { - val result = requestHelper.request(userWalletId) { authHeader -> - tangemPayApi.getCardBalance(authHeader) - }.result ?: error("Cannot get card balance") - TangemPayCardBalance( - fiatBalance = result.fiat.availableBalance, - currencyCode = result.fiat.currency, - cryptoBalance = result.crypto.balance, - availableForWithdrawal = result.availableForWithdrawal.amount, - chainId = result.crypto.chainId, - depositAddress = result.crypto.depositAddress, - contractAddress = result.crypto.tokenContractAddress, - ) - } + return catch( + block = { + val response = requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.getCardBalance(authHeader) + }.getOrNull() + + val fiatBalance = requireNotNull(response?.result?.fiat) { "Cannot get card balance fiat" } + val cryptoBalance = requireNotNull(response.result?.crypto) { "Cannot get card balance crypto" } + val withdrawalAmount = requireNotNull(response.result?.availableForWithdrawal) { + "Cannot get card balance availableForWithdrawal" + } + TangemPayCardBalance( + fiatBalance = fiatBalance.availableBalance, + currencyCode = fiatBalance.currency, + cryptoBalance = cryptoBalance.balance, + availableForWithdrawal = withdrawalAmount.amount, + chainId = cryptoBalance.chainId, + depositAddress = cryptoBalance.depositAddress, + contractAddress = cryptoBalance.tokenContractAddress, + ).right() + }, + catch = ::catchException, + ) } override suspend fun revealCardDetails(userWalletId: UserWalletId): Either { @@ -100,7 +109,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( expirationMonth = result.expirationMonth, ).right() }, - catch = { errorConverter.convert(it).left() }, + catch = ::catchException, ) } @@ -285,6 +294,11 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( } } + private fun catchException(throwable: Throwable): Either { + Timber.tag(TAG).e(throwable) + return errorConverter.convert(throwable).left() + } + private companion object { const val MAX_POLLING_RETRIES = 3 } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt index a8b31086f1..37663aa1f0 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt @@ -41,7 +41,6 @@ internal sealed class TangemPayDetailsBalanceBlockState { data class Content( override val actionButtons: ImmutableList, - val cryptoBalance: String, val fiatBalance: String, val isBalanceFlickering: Boolean, ) : TangemPayDetailsBalanceBlockState() diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt index 857920d456..d2da85c3d4 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt @@ -2,10 +2,8 @@ package com.tangem.features.tangempay.model.transformers import arrow.core.Either import com.tangem.core.error.UniversalError -import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.model.TangemPayCardBalance @@ -13,7 +11,6 @@ import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState import com.tangem.features.tangempay.entity.TangemPayDetailsUM import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf -import java.math.BigDecimal import java.util.Currency internal class DetailsBalanceTransformer( @@ -37,7 +34,6 @@ internal class DetailsBalanceTransformer( TangemPayDetailsBalanceBlockState.Content( isBalanceFlickering = false, fiatBalance = getFiatBalanceText(balance.value), - cryptoBalance = getCryptoBalanceText(balance.value.cryptoBalance, cryptoCurrency), actionButtons = prevState.balanceBlockState.actionButtons, ) } @@ -52,8 +48,4 @@ internal class DetailsBalanceTransformer( fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol) } } - - private fun getCryptoBalanceText(cryptoBalance: BigDecimal, cryptoCurrency: CryptoCurrency): String { - return cryptoBalance.format { crypto(cryptoCurrency = cryptoCurrency) } - } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index 6addbcf3d6..255f5c37a5 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -175,11 +175,6 @@ private fun TangemPayDetailsBalanceBlock( state = state, isBalanceHidden = isBalanceHidden, ) - CryptoBalance( - modifier = Modifier.padding(start = 12.dp, top = 4.dp), - state = state, - isBalanceHidden = isBalanceHidden, - ) if (state.actionButtons.isNotEmpty()) { HorizontalActionChips( modifier = Modifier.padding(top = 12.dp), @@ -220,37 +215,6 @@ private fun FiatBalance( ) } } - -@Suppress("UnusedPrivateMember") -@Composable -private fun CryptoBalance( - state: TangemPayDetailsBalanceBlockState, - isBalanceHidden: Boolean, - modifier: Modifier = Modifier, -) { - when (state) { - is TangemPayDetailsBalanceBlockState.Loading -> RectangleShimmer( - modifier = modifier.size( - width = TangemTheme.dimens.size70, - height = TangemTheme.dimens.size16, - ), - ) - is TangemPayDetailsBalanceBlockState.Content -> Text( - modifier = modifier, - text = state.cryptoBalance.orMaskWithStars(isBalanceHidden), - style = TangemTheme.typography.caption2.applyBladeBrush( - isEnabled = state.isBalanceFlickering, - textColor = TangemTheme.colors.text.tertiary, - ), - ) - is TangemPayDetailsBalanceBlockState.Error -> Text( - modifier = modifier, - text = DASH_SIGN.orMaskWithStars(isBalanceHidden), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} // endregion @OptIn(ExperimentalMaterial3Api::class) @@ -345,7 +309,6 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider Date: Fri, 19 Dec 2025 13:09:29 +0500 Subject: [PATCH 12/15] Updated on 2026-08-14 --- .../com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index f08755fcab..13f1a7ab8f 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -149,7 +149,6 @@ internal class AccessCodeModel @Inject constructor( onClick = { uiState.update { currentState -> currentState.copy( - accessCode = "", onAccessCodeChange = ::onAccessCodeChange, requestFocus = triggeredEvent(Unit, ::consumeRequestFocusEvent), ) From 71e4d3a6edeb6d2021eb7f220fbcb127e313bd09 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 16:57:19 +0500 Subject: [PATCH 13/15] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/MainActivity.kt | 4 ---- features/hot-wallet/impl/build.gradle.kts | 1 + .../addexistingwallet/entry/AddExistingWalletModel.kt | 9 ++++----- .../walletactivation/entry/WalletActivationModel.kt | 9 ++++----- 4 files changed, 9 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 076a21b682..690ab43218 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -40,7 +40,6 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase -import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -130,9 +129,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject lateinit var cardRepository: CardRepository - @Inject - lateinit var shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase - @Inject lateinit var backupServiceHolder: BackupServiceHolder diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index 380b51678e..36d956f24c 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(projects.domain.feedback) implementation(projects.domain.feedback.models) implementation(projects.domain.hotWallet) + implementation(projects.domain.notifications) /** Common */ implementation(projects.common.ui) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index 51cfc12211..ed1d37d3f9 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -15,8 +15,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.settings.ShouldAskPermissionUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase +import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent @@ -25,7 +25,6 @@ import com.tangem.features.hotwallet.accesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks -import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow @@ -37,7 +36,7 @@ import javax.inject.Inject internal class AddExistingWalletModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, - private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, + private val notificationsRepository: NotificationsRepository, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val trackingContextProxy: TrackingContextProxy, private val setAccessCodeSkippedUseCase: SetAccessCodeSkippedUseCase, @@ -79,8 +78,8 @@ internal class AddExistingWalletModel @Inject constructor( private fun navigateToPushNotificationsOrNext() { modelScope.launch { - val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) - if (shouldRequestPush) { + val shouldAskNotificationPermissions = notificationsRepository.shouldAskNotificationPermissionsViaBs() + if (shouldAskNotificationPermissions) { stackNavigation.replaceAll(AddExistingWalletRoute.PushNotifications) } else { stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt index 283d1c7eea..129947fdb7 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -18,8 +18,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.settings.ShouldAskPermissionUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase +import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent @@ -28,7 +28,6 @@ import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartCompone import com.tangem.features.hotwallet.accesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.hotwallet.walletactivation.entry.routing.WalletActivationRoute -import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.hotwallet.WalletActivationComponent import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks @@ -44,7 +43,7 @@ internal class WalletActivationModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val router: Router, - private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, + private val notificationsRepository: NotificationsRepository, private val setAccessCodeSkippedUseCase: SetAccessCodeSkippedUseCase, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val trackingContextProxy: TrackingContextProxy, @@ -118,8 +117,8 @@ internal class WalletActivationModel @Inject constructor( private fun navigateToPushNotificationsOrNext() { modelScope.launch { - val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) - if (shouldRequestPush) { + val shouldAskNotificationPermissions = notificationsRepository.shouldAskNotificationPermissionsViaBs() + if (shouldAskNotificationPermissions) { stackNavigation.replaceAll(WalletActivationRoute.PushNotifications) } else { stackNavigation.replaceAll(WalletActivationRoute.SetupFinished) From 0594e1b680642e643aa1046fe8560520c7c8089c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 17:25:04 +0500 Subject: [PATCH 14/15] Updated on 2026-08-14 --- .../sdk/impl/DefaultTangemSdkManager.kt | 33 ++++- .../domain/sdk/impl/MockTangemSdkManager.kt | 14 +- ...mPayGenerateAddressAndSignChallengeTask.kt | 15 +- .../visa/TangemPaySignWithdrawalHashTask.kt | 15 +- .../visa/VisaCustomerWalletApproveTask.kt | 80 ++--------- data/visa/build.gradle.kts | 2 + .../DefaultTangemPayAuthDataSource.kt | 34 ++--- .../pay/datasource/TangemPayHotSdkManager.kt | 130 ++++++++++++++++++ .../tangem/data/pay/di/TangemPayDataModule.kt | 3 +- .../repository/DefaultOnboardingRepository.kt | 15 +- .../DefaultTangemPaySwapRepository.kt | 36 ++--- .../DefaultTangemPayWithdrawUseCase.kt | 6 +- .../visa/DefaultTangemPayRemoteDataSource.kt | 3 +- .../ethereum/WcEthMessageSignUseCase.kt | 1 + .../domain/card/common/visa/VisaUtilities.kt | 35 ++++- .../domain/pay}/WithdrawalSignatureResult.kt | 2 +- .../pay/datasource/TangemPayAuthDataSource.kt | 10 +- .../pay/repository/TangemPaySwapRepository.kt | 4 +- .../TangemPayMainScreenCustomerInfoUseCase.kt | 23 ++-- .../tangempay/TangemPayWithdrawUseCase.kt | 4 +- .../tangem/feature/swap/model/SwapModel.kt | 2 +- .../model/TangemPayOnboardingModel.kt | 33 ++--- .../com/tangem/sdk/api/TangemSdkManager.kt | 6 +- 23 files changed, 298 insertions(+), 208 deletions(-) create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/datasource/TangemPayHotSdkManager.kt rename domain/visa/{src/main/kotlin/com/tangem/domain/pay/model => models/src/main/kotlin/com/tangem/domain/pay}/WithdrawalSignatureResult.kt (83%) diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index a303cd8df9..981b99ecf8 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -3,6 +3,9 @@ package com.tangem.tap.domain.sdk.impl import android.content.res.Resources import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import arrow.core.Either +import arrow.core.left +import arrow.core.right import com.tangem.Log import com.tangem.Message import com.tangem.TangemSdk @@ -24,6 +27,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.visa.model.* import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles @@ -516,23 +520,44 @@ internal class DefaultTangemSdkManager( override suspend fun tangemPayProduceInitialCredentials( cardId: String, - ): CompletionResult { + ): Either { return coroutineScope { - runTaskAsyncReturnOnMain( + val result = runTaskAsyncReturnOnMain( runnable = tangemPayChallengeTaskFactory.create(coroutineScope = this), cardId = cardId, initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), ) + + return@coroutineScope when (result) { + is CompletionResult.Failure<*> -> result.error.left() + is CompletionResult.Success -> result.data.right() + } } } - override suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult { + override suspend fun getWithdrawalSignature( + cardId: String, + hash: String, + ): Either { return coroutineScope { - runTaskAsyncReturnOnMain( + val result = runTaskAsyncReturnOnMain( runnable = TangemPaySignWithdrawalHashTask(cardId = cardId, hash = hash.hexToBytes()), cardId = cardId, initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), ) + + return@coroutineScope when (result) { + is CompletionResult.Failure<*> -> { + if (result.error is TangemSdkError.UserCancelled) { + WithdrawalSignatureResult.Cancelled.right() + } else { + result.error.left() + } + } + is CompletionResult.Success -> { + WithdrawalSignatureResult.Success(result.data).right() + } + } } } // endregion diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index 91fe1414d6..54e9e33aca 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -3,6 +3,7 @@ package com.tangem.tap.domain.sdk.impl import android.content.res.Resources import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import arrow.core.Either import com.tangem.Message import com.tangem.common.CompletionResult import com.tangem.common.KeyPair @@ -18,7 +19,11 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.visa.model.* +import com.tangem.domain.pay.WithdrawalSignatureResult +import com.tangem.domain.visa.model.TangemPayInitialCredentials +import com.tangem.domain.visa.model.VisaActivationInput +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.wallet.CreateWalletResponse @@ -215,11 +220,14 @@ class MockTangemSdkManager( override suspend fun tangemPayProduceInitialCredentials( cardId: String, - ): CompletionResult { + ): Either { error("Not implemented") } - override suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult { + override suspend fun getWithdrawalSignature( + cardId: String, + hash: String, + ): Either { error("Not implemented") } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt index bc05e53d63..4550d3dc52 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt @@ -3,7 +3,6 @@ package com.tangem.tap.domain.tasks.visa import arrow.core.getOrElse import com.tangem.common.CompletionResult import com.tangem.common.card.CardWallet -import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback @@ -42,12 +41,14 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( private suspend fun runSuspend(session: CardSession): CompletionResult { val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } + val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.curve } ?: return CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError) val address = when (val derivationResult = runDerivationTask(session, wallet)) { is CompletionResult.Failure<*> -> return CompletionResult.Failure(derivationResult.error) - is CompletionResult.Success -> generateAddressFromExtendedKey(derivationResult.data) + is CompletionResult.Success -> VisaUtilities.generateAddressFromExtendedKey( + extendedPublicKey = derivationResult.data, + ) } val userWalletId = UserWalletIdBuilder.walletPublicKey(wallet.publicKey) @@ -119,14 +120,6 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( return deferred.await() } - private fun generateAddressFromExtendedKey(extendedPublicKey: ExtendedPublicKey): String { - val derivationData = VisaUtilities.visaBlockchain.makeAddressesFromExtendedPublicKey( - extendedPublicKey = extendedPublicKey, - cachedIndex = null, - ) - return derivationData.address - } - @AssistedFactory interface Factory { fun create(coroutineScope: CoroutineScope): TangemPayGenerateAddressAndSignChallengeTask diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt index d5bc3b64c1..2cf1b0f42c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt @@ -1,15 +1,11 @@ package com.tangem.tap.domain.tasks.visa -import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.common.CompletionResult import com.tangem.common.card.Card -import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.toDecompressedPublicKey -import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey @@ -40,7 +36,7 @@ class TangemPaySignWithdrawalHashTask( private fun proceedSign(card: Card, session: CardSession, callback: CompletionCallback) { val derivationPath = VisaUtilities.customDerivationPath - val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { + val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.curve } ?: run { callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)) return } @@ -71,7 +67,7 @@ class TangemPaySignWithdrawalHashTask( private fun signData( targetWalletPublicKey: ByteArray, derivationPath: DerivationPath?, - extendedPublicKey: ExtendedPublicKey?, + extendedPublicKey: ExtendedPublicKey, session: CardSession, callback: CompletionCallback, ) { @@ -84,12 +80,11 @@ class TangemPaySignWithdrawalHashTask( signTask.run(session) { result -> when (result) { is CompletionResult.Success -> { - val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( + val rsvSignature = VisaUtilities.unmarshallSignature( signature = result.data.signature, hash = hash, - publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey() - ?: targetWalletPublicKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString().lowercase() + extendedPublicKey = extendedPublicKey, + ) callback(CompletionResult.Success(rsvSignature)) } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 3439d72093..d4c6dc4800 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -1,28 +1,19 @@ package com.tangem.tap.domain.tasks.visa -import arrow.core.getOrElse -import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak -import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.common.CompletionResult import com.tangem.common.card.Card import com.tangem.common.card.CardWallet -import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.toDecompressedPublicKey -import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility -import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.visa.error.VisaActivationError import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet -import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.sign.SignHashCommand @@ -46,11 +37,7 @@ class VisaCustomerWalletApproveTask( return } - if (card.settings.isHDWalletAllowed) { - proceedApprove(card, session, callback) - } else { - proceedApproveWithLegacyCard(card, session, callback) - } + proceedApprove(card, session, callback) } private fun proceedApprove( @@ -60,7 +47,7 @@ class VisaCustomerWalletApproveTask( ) { val derivationPath = VisaUtilities.customDerivationPath - val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { + val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.curve } ?: run { callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)) return } @@ -114,43 +101,15 @@ class VisaCustomerWalletApproveTask( ) } - private fun proceedApproveWithLegacyCard( - card: Card, - session: CardSession, - callback: CompletionCallback, - ) { - val publicKey = findKeyWithoutDerivation( - targetAddress = visaDataForApprove.targetAddress, - card = CardDTO(card), - ).getOrElse { error -> - callback(CompletionResult.Failure(error.tangemError)) - return - } - - signApproveData( - targetWalletPublicKey = publicKey, - derivationPath = null, - extendedPublicKey = null, - session = session, - callback = callback, - ) - } - - // TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK - private fun hashPersonalMessage(message: ByteArray): ByteArray { - val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray() - return (prefix + message).toKeccak() - } - private fun signApproveData( targetWalletPublicKey: ByteArray, derivationPath: DerivationPath?, - extendedPublicKey: ExtendedPublicKey?, + extendedPublicKey: ExtendedPublicKey, session: CardSession, callback: CompletionCallback, ) { - val content = "Tangem Pay wants to sign in with your account. Nonce: ${visaDataForApprove.hashToSign}" - val hash = hashPersonalMessage(content.toByteArray(Charsets.UTF_8)) + val content = VisaUtilities.signWithNonceMessage(visaDataForApprove.hashToSign) + val hash = VisaUtilities.hashPersonalMessage(content.toByteArray(Charsets.UTF_8)) val signTask = SignHashCommand( hash = hash, @@ -161,36 +120,13 @@ class VisaCustomerWalletApproveTask( signTask.run(session) { result -> when (result) { is CompletionResult.Success -> { - val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( + val rsvSignature = VisaUtilities.unmarshallSignature( signature = result.data.signature, hash = hash, - publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey() - ?: targetWalletPublicKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString().lowercase() - - scanCard( - session = session, - callback = callback, - signedData = visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress), + extendedPublicKey = extendedPublicKey, ) - } - is CompletionResult.Failure -> { - callback(CompletionResult.Failure(result.error)) - } - } - } - } - private fun scanCard( - signedData: VisaSignedDataByCustomerWallet, - session: CardSession, - callback: CompletionCallback, - ) { - val scanTask = ScanTask() - scanTask.run(session) { result -> - when (result) { - is CompletionResult.Success -> { - callback(CompletionResult.Success(signedData)) + visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress) } is CompletionResult.Failure -> { callback(CompletionResult.Failure(result.error)) diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 1470c2b5d9..f893a530d6 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { /** Project - Data */ implementation(projects.core.datasource) implementation(projects.core.error) + implementation(projects.core.error.ext) implementation(projects.core.security) implementation(projects.data.common) @@ -60,6 +61,7 @@ dependencies { /** Libs - Tangem */ implementation(tangemDeps.blockchain) implementation(tangemDeps.card.core) + implementation(tangemDeps.hot.core) implementation(projects.libs.tangemSdkApi) /** DI */ diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt index 9eea6d7429..6af402c591 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt @@ -1,42 +1,34 @@ package com.tangem.data.pay.datasource import arrow.core.Either -import arrow.core.left -import arrow.core.right -import com.tangem.common.CompletionResult -import com.tangem.common.core.TangemSdkError +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.pay.datasource.TangemPayAuthDataSource -import com.tangem.domain.pay.model.WithdrawalSignatureResult import com.tangem.domain.visa.model.TangemPayInitialCredentials import com.tangem.sdk.api.TangemSdkManager import javax.inject.Inject internal class DefaultTangemPayAuthDataSource @Inject constructor( private val tangemSdkManager: TangemSdkManager, + private val tangemPayHotSdkManager: TangemPayHotSdkManager, ) : TangemPayAuthDataSource { - override suspend fun produceInitialCredentials(cardId: String): Either { - return when (val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId)) { - is CompletionResult.Failure<*> -> initialCredentials.error.left() - is CompletionResult.Success -> initialCredentials.data.right() + override suspend fun produceInitialCredentials( + userWallet: UserWallet, + ): Either { + return when (userWallet) { + is UserWallet.Cold -> tangemSdkManager.tangemPayProduceInitialCredentials(cardId = userWallet.cardId) + is UserWallet.Hot -> tangemPayHotSdkManager.produceInitialCredentials(userWallet) } } override suspend fun getWithdrawalSignature( - cardId: String, + userWallet: UserWallet, hash: String, ): Either { - return when (val signResult = tangemSdkManager.getWithdrawalSignature(cardId, hash)) { - is CompletionResult.Failure<*> -> { - if (signResult.error is TangemSdkError.UserCancelled) { - WithdrawalSignatureResult.Cancelled.right() - } else { - signResult.error.left() - } - } - is CompletionResult.Success -> { - WithdrawalSignatureResult.Success(signResult.data).right() - } + return when (userWallet) { + is UserWallet.Cold -> tangemSdkManager.getWithdrawalSignature(cardId = userWallet.cardId, hash = hash) + is UserWallet.Hot -> tangemPayHotSdkManager.getWithdrawalSignature(hotWallet = userWallet, hash = hash) } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/TangemPayHotSdkManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/TangemPayHotSdkManager.kt new file mode 100644 index 0000000000..6c2eac2122 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/TangemPayHotSdkManager.kt @@ -0,0 +1,130 @@ +package com.tangem.data.pay.datasource + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.either +import com.tangem.common.extensions.hexToBytes +import com.tangem.core.error.ext.tangemError +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.card.common.visa.VisaUtilities +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.WithdrawalSignatureResult +import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource +import com.tangem.domain.visa.error.VisaActivationError +import com.tangem.domain.visa.error.VisaCardScanError +import com.tangem.domain.visa.model.TangemPayInitialCredentials +import com.tangem.domain.wallets.hot.HotWalletAccessor +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.DataToSign +import com.tangem.hot.sdk.model.DeriveWalletRequest +import com.tangem.hot.sdk.model.UnlockHotWallet +import javax.inject.Inject + +internal class TangemPayHotSdkManager @Inject constructor( + private val hotWalletAccessor: HotWalletAccessor, + private val tangemHotSdk: TangemHotSdk, + private val tangemPayAuthRemoteDataSource: TangemPayRemoteDataSource, +) { + + suspend fun produceInitialCredentials(hotWallet: UserWallet.Hot): Either = + withUnlockedHotWallet(hotWallet) { unlockHotWallet -> + val extendedPublicKey = getExtendedPublicKey(unlockHotWallet = unlockHotWallet) + val address = VisaUtilities.generateAddressFromExtendedKey(extendedPublicKey) + val challenge = tangemPayAuthRemoteDataSource.getCustomerWalletAuthChallenge( + customerWalletAddress = address, + customerWalletId = hotWallet.walletId.stringValue, + ).getOrElse { raise(it.tangemError) } + + val content = VisaUtilities.signWithNonceMessage(challenge.challenge) + val hash = VisaUtilities.hashPersonalMessage(content.toByteArray(Charsets.UTF_8)) + val signature = getSignature( + unlockHotWallet = unlockHotWallet, + hash = hash, + extendedPublicKey = extendedPublicKey, + ) + + val authTokens = tangemPayAuthRemoteDataSource.getTokenWithCustomerWallet( + sessionId = challenge.session.sessionId, + signature = signature, + nonce = challenge.challenge, + ).getOrElse { raise(VisaActivationError.FailedRemoteState.tangemError) } + + TangemPayInitialCredentials( + customerWalletAddress = address, + authTokens = authTokens, + ) + } + + suspend fun getWithdrawalSignature( + hotWallet: UserWallet.Hot, + hash: String, + ): Either = withUnlockedHotWallet(hotWallet) { unlockHotWallet -> + val signature = getSignature( + unlockHotWallet = unlockHotWallet, + hash = hash.hexToBytes(), + extendedPublicKey = getExtendedPublicKey(unlockHotWallet = unlockHotWallet), + ) + + WithdrawalSignatureResult.Success(signature) + } + + private suspend fun Raise.getExtendedPublicKey(unlockHotWallet: UnlockHotWallet): ExtendedPublicKey { + val publicKeyResponse = tangemHotSdk.derivePublicKey( + unlockHotWallet = unlockHotWallet, + request = DeriveWalletRequest( + requests = listOf( + DeriveWalletRequest.Request( + curve = VisaUtilities.curve, + paths = listOf(VisaUtilities.customDerivationPath), + ), + ), + ), + ) + return publicKeyResponse.responses + .firstOrNull { it.curve == VisaUtilities.curve } + ?.publicKeys[VisaUtilities.customDerivationPath] + ?: raise(VisaActivationError.MissingWallet.tangemError) + } + + private suspend fun Raise.getSignature( + unlockHotWallet: UnlockHotWallet, + hash: ByteArray, + extendedPublicKey: ExtendedPublicKey, + ): String { + val signedHashes = tangemHotSdk.signHashes( + unlockHotWallet = unlockHotWallet, + dataToSign = listOf( + DataToSign( + curve = VisaUtilities.curve, + derivationPath = VisaUtilities.customDerivationPath, + hashes = listOf(hash), + ), + ), + ) + val signature = signedHashes + .firstOrNull { it.curve == VisaUtilities.curve } + ?.signatures + ?.firstOrNull() + ?: raise(VisaCardScanError.FailedToSignChallenge.tangemError) + + return VisaUtilities.unmarshallSignature( + signature = signature, + hash = hash, + extendedPublicKey = extendedPublicKey, + ) + } + + private suspend inline fun withUnlockedHotWallet( + hotWallet: UserWallet.Hot, + block: Raise.(UnlockHotWallet) -> T, + ): Either = either { + try { + val unlockHotWallet = hotWalletAccessor.getContextualUnlock(hotWallet.hotWalletId) + ?: hotWalletAccessor.unlockContextual(hotWallet.hotWalletId) + block(unlockHotWallet) + } finally { + hotWalletAccessor.clearContextualUnlock(hotWallet.hotWalletId) + } + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index ffcd79fb85..719542728a 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -80,9 +80,8 @@ internal interface TangemPayDataModule { deviceSecurity: DeviceSecurityInfoProvider, ): TangemPayMainScreenCustomerInfoUseCase { return TangemPayMainScreenCustomerInfoUseCase( - repository = repository, + onboardingRepository = repository, customerOrderRepository = customerOrderRepository, - tangemPayOnboardingRepository = tangemPayOnboardingRepository, eligibilityManager = eligibilityManager, deviceSecurity = deviceSecurity, ) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index c3ab4f66c0..348ae72b3b 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -65,15 +65,16 @@ internal class DefaultOnboardingRepository @Inject constructor( override suspend fun produceInitialData(userWalletId: UserWalletId) { withContext(dispatcherProvider.io) { - val initialCredentials = authDataSource.produceInitialCredentials(cardId = getCardId(userWalletId)) + val userWallet = getUserWallet(userWalletId) + val initialCredentials = authDataSource.produceInitialCredentials(userWallet) .fold( ifLeft = { error -> error("Can not produce initial data: ${error.message}") }, ifRight = { it }, ) // should storeCheckCustomerWalletResult because we already know this - tangemPayStorage.storeCheckCustomerWalletResult(userWalletId, true) + tangemPayStorage.storeCheckCustomerWalletResult(userWallet.walletId, true) tangemPayStorage.storeCustomerWalletAddress( - userWalletId = userWalletId, + userWalletId = userWallet.walletId, customerWalletAddress = initialCredentials.customerWalletAddress, ) tangemPayStorage.storeAuthTokens( @@ -120,17 +121,13 @@ internal class DefaultOnboardingRepository @Inject constructor( } } - private fun getCardId(userWalletId: UserWalletId): String { + private fun getUserWallet(userWalletId: UserWalletId): UserWallet { val userWallet = if (hotWalletFeatureToggles.isHotWalletEnabled) { userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId } } else { userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == userWalletId } } ?: error("no userWallet found") - return if (userWallet is UserWallet.Cold) { - userWallet.cardId - } else { - TODO("[REDACTED_JIRA]") - } + return userWallet } private suspend fun getCustomerInfo( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt index 042686b6e7..6bc1774f28 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt @@ -1,23 +1,20 @@ package com.tangem.data.pay.repository import arrow.core.Either +import arrow.core.left import com.tangem.core.error.UniversalError import com.tangem.data.common.quote.QuotesFetcher import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.request.WithdrawDataRequest import com.tangem.datasource.api.pay.models.request.WithdrawRequest import com.tangem.datasource.local.visa.TangemPayStorage -import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.WithdrawalResult +import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.pay.datasource.TangemPayAuthDataSource -import com.tangem.domain.pay.model.WithdrawalSignatureResult import com.tangem.domain.pay.repository.TangemPaySwapRepository import com.tangem.domain.visa.error.VisaApiError -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.extensions.addHexPrefix import java.math.BigDecimal import java.math.RoundingMode @@ -30,29 +27,25 @@ internal class DefaultTangemPaySwapRepository @Inject constructor( private val tangemPayApi: TangemPayApi, private val requestHelper: TangemPayRequestPerformer, private val authDataSource: TangemPayAuthDataSource, - private val userWalletsListManager: UserWalletsListManager, - private val userWalletsListRepository: UserWalletsListRepository, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val quotesFetcher: QuotesFetcher, private val tangemPayStorage: TangemPayStorage, ) : TangemPaySwapRepository { override suspend fun withdraw( - userWalletId: UserWalletId, + userWallet: UserWallet, receiverAddress: String, cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, ): Either { val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId) if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError) - return requestHelper.performRequest(userWalletId) { authHeader -> + return requestHelper.performRequest(userWallet.walletId) { authHeader -> val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress) tangemPayApi.getWithdrawData(authHeader = authHeader, body = request) }.map { data -> - val result = data.result - if (result == null) return Either.Left(VisaApiError.WithdrawalDataError) + val result = data.result ?: return VisaApiError.WithdrawalDataError.left() val signatureResult = authDataSource.getWithdrawalSignature( - cardId = getCardId(userWalletId), + userWallet = userWallet, hash = result.hash, ).getOrNull() @@ -61,7 +54,7 @@ internal class DefaultTangemPaySwapRepository @Inject constructor( Either.Right(WithdrawalResult.Cancelled) } is WithdrawalSignatureResult.Success -> { - requestHelper.performRequest(userWalletId) { authHeader -> + requestHelper.performRequest(userWallet.walletId) { authHeader -> val request = WithdrawRequest( amountInCents = amountInCents, recipientAddress = receiverAddress, @@ -74,7 +67,7 @@ internal class DefaultTangemPaySwapRepository @Inject constructor( .mapLeft { return Either.Left(VisaApiError.WithdrawError) } .map { response -> val orderId = response.result?.orderId - if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWalletId, orderId) + if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWallet.walletId, orderId) WithdrawalResult.Success } } @@ -103,17 +96,4 @@ internal class DefaultTangemPaySwapRepository @Inject constructor( return quotes?.quotes[cryptoCurrencyId.value]?.price } - - private fun getCardId(userWalletId: UserWalletId): String { - val userWallet = if (hotWalletFeatureToggles.isHotWalletEnabled) { - userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId } - } else { - userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == userWalletId } - } ?: error("No User Wallet found") - return if (userWallet is UserWallet.Cold) { - userWallet.cardId - } else { - TODO("[REDACTED_JIRA]") - } - } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt index 3ccde5d629..36b85985ad 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt @@ -3,7 +3,7 @@ package com.tangem.data.pay.usecase import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.WithdrawalResult import com.tangem.domain.pay.repository.TangemPaySwapRepository import com.tangem.domain.tangempay.TangemPayWithdrawUseCase @@ -15,13 +15,13 @@ internal class DefaultTangemPayWithdrawUseCase @Inject constructor( ) : TangemPayWithdrawUseCase { override suspend fun invoke( - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, receiverCexAddress: String, ): Either { return repository.withdraw( - userWalletId = userWalletId, + userWallet = userWallet, cryptoAmount = cryptoAmount, receiverAddress = receiverCexAddress, cryptoCurrencyId = cryptoCurrencyId, diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt index c2a927964b..63d2853352 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt @@ -9,6 +9,7 @@ import com.tangem.datasource.api.pay.models.request.GenerateNonceByCustomerWalle import com.tangem.datasource.api.pay.models.request.GetTokenByCustomerWalletRequest import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse import com.tangem.datasource.di.NetworkMoshi +import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayAuthTokens @@ -56,7 +57,7 @@ internal class DefaultTangemPayRemoteDataSource @Inject constructor( authType = "customer_wallet", sessionId = sessionId, signature = signature, - messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce", + messageFormat = VisaUtilities.signWithNonceMessage(nonce), ), ).getOrThrow() }.map { response -> diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt index 36d3087190..033ffd4461 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt @@ -83,6 +83,7 @@ internal class WcEthMessageSignUseCase @AssistedInject constructor( object LegacySdkHelper { private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n" + // TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK fun prepareToSendMessageData(signedHash: ByteArray, hashToSign: ByteArray, walletManager: WalletManager): String = UnmarshalHelper.unmarshalSignatureExtended( signature = signedHash, diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt index fe41ac4f05..2ed1be037d 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt @@ -1,9 +1,15 @@ package com.tangem.domain.card.common.visa +import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion +import com.tangem.common.extensions.toDecompressedPublicKey +import com.tangem.common.extensions.toHexString import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO private const val VISA_BATCH_START = "AE" @@ -11,15 +17,16 @@ private const val VISA_BATCH_START_2 = "FFFC" object VisaUtilities { - const val tokenId = "tether" - val visaBlockchain = Blockchain.Polygon val visaDefaultDerivationPath get() = visaBlockchain.derivationPath(DerivationStyle.V3) val customDerivationPath = DerivationPath("m/44'/60'/999999'/0/0") + val curve = EllipticCurve.Secp256k1 - fun visaDefaultDerivationPath(style: DerivationStyle) = visaBlockchain.derivationPath(style) + fun signWithNonceMessage(nonce: String): String { + return "Tangem Pay wants to sign in with your account. Nonce: $nonce" + } fun isVisaCard(card: CardDTO): Boolean { return isVisaCard(card.firmwareVersion.doubleValue, card.batchId) @@ -29,4 +36,26 @@ object VisaUtilities { return firmwareVersion in FirmwareVersion.visaRange && (batchId.startsWith(VISA_BATCH_START) || batchId.startsWith(VISA_BATCH_START_2)) } + + // TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK + fun hashPersonalMessage(message: ByteArray): ByteArray { + val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray() + return (prefix + message).toKeccak() + } + + fun generateAddressFromExtendedKey(extendedPublicKey: ExtendedPublicKey): String { + val derivationData = visaBlockchain.makeAddressesFromExtendedPublicKey( + extendedPublicKey = extendedPublicKey, + cachedIndex = null, + ) + return derivationData.address + } + + fun unmarshallSignature(signature: ByteArray, hash: ByteArray, extendedPublicKey: ExtendedPublicKey): String { + return UnmarshalHelper.unmarshalSignatureExtended( + signature = signature, + hash = hash, + publicKey = extendedPublicKey.publicKey.toDecompressedPublicKey(), + ).asRSVLegacyEVM().toHexString().lowercase() + } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/WithdrawalSignatureResult.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/WithdrawalSignatureResult.kt similarity index 83% rename from domain/visa/src/main/kotlin/com/tangem/domain/pay/model/WithdrawalSignatureResult.kt rename to domain/visa/models/src/main/kotlin/com/tangem/domain/pay/WithdrawalSignatureResult.kt index 42f55abd15..18e996d13d 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/WithdrawalSignatureResult.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/WithdrawalSignatureResult.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.pay.model +package com.tangem.domain.pay sealed class WithdrawalSignatureResult { diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt index 66a037381f..4ab30e9ff7 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt @@ -1,12 +1,16 @@ package com.tangem.domain.pay.datasource import arrow.core.Either -import com.tangem.domain.pay.model.WithdrawalSignatureResult +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.visa.model.TangemPayInitialCredentials interface TangemPayAuthDataSource { - suspend fun produceInitialCredentials(cardId: String): Either + suspend fun produceInitialCredentials(userWallet: UserWallet): Either - suspend fun getWithdrawalSignature(cardId: String, hash: String): Either + suspend fun getWithdrawalSignature( + userWallet: UserWallet, + hash: String, + ): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt index d27ac30af7..fed66aab7d 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt @@ -3,14 +3,14 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.WithdrawalResult import java.math.BigDecimal interface TangemPaySwapRepository { suspend fun withdraw( - userWalletId: UserWalletId, + userWallet: UserWallet, receiverAddress: String, cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 10a21fbb78..9636a5136b 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -21,9 +21,8 @@ private const val TAG = "TangemPayMainScreenCustomerInfoUseCase" * Works only if the user already authorised at least once (won't emit anything otherwise) */ class TangemPayMainScreenCustomerInfoUseCase( - private val repository: OnboardingRepository, + private val onboardingRepository: OnboardingRepository, private val customerOrderRepository: CustomerOrderRepository, - private val tangemPayOnboardingRepository: OnboardingRepository, private val eligibilityManager: TangemPayEligibilityManager, private val deviceSecurity: DeviceSecurityInfoProvider, ) { @@ -32,7 +31,7 @@ class TangemPayMainScreenCustomerInfoUseCase( field = MutableStateFlow(value = mapOf()) suspend fun fetch(userWalletId: UserWalletId) { - Timber.tag(TAG).i("fetch: $userWalletId") + Timber.tag(TAG).i("fetch: ${userWalletId.stringValue}") if (deviceSecurity.isSecurityExposed()) { Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") @@ -43,7 +42,7 @@ class TangemPayMainScreenCustomerInfoUseCase( return // fast exit } - repository.checkCustomerWallet(userWalletId) + onboardingRepository.checkCustomerWallet(userWalletId) .fold( ifLeft = { error -> Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") @@ -64,7 +63,7 @@ class TangemPayMainScreenCustomerInfoUseCase( // if there's no tangem pay, check eligibility and show onboarding banner val isEligible = eligibilityManager.getEligibleWallets().any { it.walletId == userWalletId } if (isEligible) { - if (tangemPayOnboardingRepository.getHideMainOnboardingBanner(userWalletId)) { + if (onboardingRepository.getHideMainOnboardingBanner(userWalletId)) { updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) } else { updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right()) @@ -95,10 +94,10 @@ class TangemPayMainScreenCustomerInfoUseCase( private suspend fun proceedWithPaeraCustomerResult( userWalletId: UserWalletId, ): Either { - if (!tangemPayOnboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { + if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { return TangemPayCustomerInfoError.RefreshNeededError.left() } - val orderId = repository.getOrderId(userWalletId) + val orderId = onboardingRepository.getOrderId(userWalletId) return if (orderId != null) { proceedWithOrderId(userWalletId = userWalletId, orderId = orderId) } else { @@ -109,7 +108,7 @@ class TangemPayMainScreenCustomerInfoUseCase( private suspend fun proceedWithoutOrder( userWalletId: UserWalletId, ): Either { - return repository.getCustomerInfo(userWalletId) + return onboardingRepository.getCustomerInfo(userWalletId) .mapLeft { error -> Timber.tag(TAG).e("mapErrorForCustomer: $error") error.mapErrorForCustomer() @@ -118,7 +117,7 @@ class TangemPayMainScreenCustomerInfoUseCase( Timber.tag(TAG).i("customerInfo") if (customerInfo.cardInfo == null && customerInfo.isKycApproved) { // If order id wasn't saved -> start order creation and get customer info - repository.createOrder(userWalletId) + onboardingRepository.createOrder(userWalletId) } MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.UNKNOWN) } @@ -148,10 +147,10 @@ class TangemPayMainScreenCustomerInfoUseCase( OrderStatus.CANCELED, OrderStatus.UNKNOWN, -> { - repository.clearOrderId(userWalletId) + onboardingRepository.clearOrderId(userWalletId) // If order was cancelled -> start order creation - if (orderStatus == OrderStatus.CANCELED) repository.createOrder(userWalletId) - repository.getCustomerInfo(userWalletId = userWalletId) + if (orderStatus == OrderStatus.CANCELED) onboardingRepository.createOrder(userWalletId) + onboardingRepository.getCustomerInfo(userWalletId = userWalletId) .mapLeft { it.mapErrorForCustomer() } .map { customerInfo -> MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus) diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt index 910f50090c..e2abfd227f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt @@ -3,14 +3,14 @@ package com.tangem.domain.tangempay import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.WithdrawalResult import java.math.BigDecimal interface TangemPayWithdrawUseCase { suspend operator fun invoke( - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, receiverCexAddress: String, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index f4b0a59776..cbed111a44 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -845,7 +845,7 @@ internal class SwapModel @Inject constructor( private suspend fun processTangemPayWithdrawal(swapTransactionState: SwapTransactionState.TangemPayWithdrawalData) { tangemPayWithdrawUseCase( - userWalletId = userWalletId, + userWallet = userWallet, cryptoAmount = swapTransactionState.cryptoAmount, cryptoCurrencyId = swapTransactionState.cryptoCurrencyId, receiverCexAddress = swapTransactionState.cexAddress, diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index bc39e6d47b..764ff69042 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -89,9 +89,7 @@ internal class TangemPayOnboardingModel @Inject constructor( private fun checkCustomerInfo(userWalletId: UserWalletId) { modelScope.launch { uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true)) - repository.getCustomerInfo( - userWalletId = userWalletId, - ) + repository.getCustomerInfo(userWalletId = userWalletId) .onRight { customerInfo -> uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) when { @@ -159,21 +157,20 @@ internal class TangemPayOnboardingModel @Inject constructor( uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) return@launch } - repository.getCustomerInfo( - userWalletId = userWalletId, - ).fold( - ifLeft = { error -> - Timber.e("Error getCustomerInfo: ${error.errorCode}") - uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) - }, - ifRight = { customerInfo -> - if (customerInfo.isKycApproved) { - back() - } else { - openKyc(userWalletId) - } - }, - ) + repository.getCustomerInfo(userWalletId = userWalletId) + .fold( + ifLeft = { error -> + Timber.e("Error getCustomerInfo: ${error.errorCode}") + uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) + }, + ifRight = { customerInfo -> + if (customerInfo.isKycApproved) { + back() + } else { + openKyc(userWalletId) + } + }, + ) } } diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index 628ecd897d..07a2d02773 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -2,6 +2,7 @@ package com.tangem.sdk.api import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import arrow.core.Either import com.tangem.Message import com.tangem.common.CompletionResult import com.tangem.common.KeyPair @@ -16,6 +17,7 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.visa.model.TangemPayInitialCredentials import com.tangem.domain.visa.model.VisaActivationInput import com.tangem.domain.visa.model.VisaDataForApprove @@ -159,8 +161,8 @@ interface TangemSdkManager { visaDataForApprove: VisaDataForApprove, ): CompletionResult - suspend fun tangemPayProduceInitialCredentials(cardId: String): CompletionResult + suspend fun tangemPayProduceInitialCredentials(cardId: String): Either - suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult + suspend fun getWithdrawalSignature(cardId: String, hash: String): Either // endregion } \ No newline at end of file From 4d099be1c71aa1dffe51a7dc1ef0bf5e147c4d42 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 12:25:24 +0000 Subject: [PATCH 15/15] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 462e99b59c..36965eb1e1 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.32-1329" +tangemBlockchainSdk = "develop-1330" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.32-574" +tangemCardSdk = "develop-573" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^