From f4a778a9be72f07838d00c4eb119f4c3eb09faa2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 May 2025 18:12:24 +0500 Subject: [PATCH 01/19] Updated on 2026-08-14 --- .../TokenDetailsOnrampTransactionStateConverter.kt | 6 +++++- .../TokenDetailsSwapTransactionsStateConverter.kt | 7 +++++-- .../state/factory/express/OnrampStatusFactory.kt | 10 ++++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt index 93ae9d4370..1403cc08fc 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt @@ -219,7 +219,11 @@ internal class TokenDetailsOnrampTransactionStateConverter( resourceReference(R.string.express_status_bought, wrappedList(cryptoCurrency.name)) } }, - state = getStatusState(OnrampStatus.Status.Paid), + state = when { + this == OnrampStatus.Status.RefundInProgress || + this == OnrampStatus.Status.Refunded -> ExpressStatusItemState.Error + else -> getStatusState(OnrampStatus.Status.Paid) + } ) private fun OnrampStatus.Status.getSendingItem() = ExpressStatusItemUM( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 780910ba7b..8e57128ffa 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -24,10 +24,10 @@ import com.tangem.feature.swap.domain.models.domain.ExchangeStatus.Companion.isF import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeStatusState import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM -import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider import com.tangem.utils.converter.Converter @@ -131,7 +131,10 @@ internal class TokenDetailsSwapTransactionsStateConverter( notification = notification, statuses = getStatuses(statusModel.status, hasFailed), showProviderLink = showProviderLink, - info = tx.info.copy(txExternalUrl = statusModel.txExternalUrl), + info = tx.info.copy( + txExternalId = statusModel.txExternalId, + txExternalUrl = statusModel.txExternalUrl, + ), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index 9ba8330394..76ca1a1052 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -14,8 +14,8 @@ import com.tangem.domain.onramp.model.OnrampStatus.Status.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent -import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsOnrampTransactionStateConverter import com.tangem.utils.Provider @@ -89,7 +89,13 @@ internal class OnrampStatusFactory @AssistedInject constructor( }, ifRight = { statusModel -> sendStatusUpdateAnalytics(onrampTx, statusModel) - onrampTx.copy(activeStatus = statusModel.status) + onrampTx.copy( + activeStatus = statusModel.status, + info = onrampTx.info.copy( + txExternalId = statusModel.externalTxId, + txExternalUrl = statusModel.externalTxUrl, + ), + ) }, ) } From 6394f7b84fc743191cc8e81b0a29cfa870cabfb1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 May 2025 18:13:02 +0500 Subject: [PATCH 02/19] Updated on 2026-08-14 --- .../redirect/DefaultOnrampRedirectComponent.kt | 17 +++++++++++++++++ .../redirect/model/OnrampRedirectModel.kt | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt index 17904c58e6..3f0059348b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt @@ -5,21 +5,38 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier +import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.onramp.redirect.model.OnrampRedirectModel import com.tangem.features.onramp.redirect.ui.OnrampRedirectContent +import com.tangem.features.onramp.success.OnrampSuccessScreenTrigger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch internal class DefaultOnrampRedirectComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: OnrampRedirectComponent.Params, + private val onrampSuccessScreenTrigger: OnrampSuccessScreenTrigger, ) : OnrampRedirectComponent, AppComponentContext by appComponentContext { private val model: OnrampRedirectModel = getOrCreateModel(params) + init { + lifecycle.subscribe( + onResume = { + // On reopen check if redirect happened. If so trigger close action + if (model.latestOnrampTransaction != null) { + componentScope.launch { + onrampSuccessScreenTrigger.triggerOnrampSuccess(false) + } + } + }, + ) + } + @Composable override fun Content(modifier: Modifier) { BackHandler(onBack = params.onBack) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt index c9a11d1cb5..ce08252b7b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt @@ -46,7 +46,7 @@ internal class OnrampRedirectModel @Inject constructor( private val params: OnrampRedirectComponent.Params = paramsContainer.require() private val selectedUserWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } - private var latestOnrampTransaction: OnrampTransaction? = null + var latestOnrampTransaction: OnrampTransaction? = null val state = OnrampRedirectUM( topBarConfig = OnrampRedirectTopBarUM( @@ -100,10 +100,12 @@ internal class OnrampRedirectModel @Inject constructor( val replaceOnrampScreens = appRouter.stack .filterNot { it is AppRoute.Onramp } .toMutableList() + latestOnrampTransaction = null replaceOnrampScreens.add(AppRoute.OnrampSuccess(latestTxId)) appRouter.replaceAll(*replaceOnrampScreens.toTypedArray()) } else { // Close redirect screen and show last onramp state + latestOnrampTransaction = null params.onBack() } } From 56d3749d6df5be1f85461de74de688045a98356d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 May 2025 18:14:28 +0500 Subject: [PATCH 03/19] Updated on 2026-08-14 --- .../common/ui/expressStatus/ExpressProvider.kt | 1 + .../ui/expressStatus/ExpressStatusBlock.kt | 17 ++++++++++++----- .../ui/components/inputrow/InputRowBestRate.kt | 3 ++- ...kenDetailsOnrampTransactionStateConverter.kt | 4 ++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressProvider.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressProvider.kt index 1f8f4f8eb3..11fb6924e5 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressProvider.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressProvider.kt @@ -39,6 +39,7 @@ fun ExpressProvider( val hapticFeedback = LocalHapticFeedback.current val context = LocalContext.current Column( + verticalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action), diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt index d31f527a9d..ad3c3c1c94 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt @@ -19,16 +19,16 @@ 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.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.tangem.common.ui.R import com.tangem.common.ui.expressStatus.state.ExpressLinkUM import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM import com.tangem.common.ui.expressStatus.state.ExpressStatusUM import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import kotlinx.collections.immutable.persistentListOf @@ -53,6 +53,7 @@ fun ExpressStatusBlock(state: ExpressStatusUM, modifier: Modifier = Modifier) { ) { Row( verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .padding(bottom = TangemTheme.dimens.spacing16), ) { @@ -77,8 +78,14 @@ fun ExpressStatusBlock(state: ExpressStatusUM, modifier: Modifier = Modifier) { .padding(end = TangemTheme.dimens.spacing2), ) Text( - text = link.text.resolveReference(), - style = TangemTheme.typography.body2, + modifier = Modifier.align(Alignment.CenterVertically), + text = resourceReference( + id = R.string.express_transaction_id, + formatArgs = wrappedList(link.text), + ).resolveReference(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt index df498f267a..03201dcd0a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.inputrow.inner.DividerContainer @@ -76,7 +77,7 @@ fun InputRowBestRate( text = subtitle.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + modifier = Modifier.padding(top = 2.dp), ) } SpacerWMax() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt index 1403cc08fc..06644ff6e6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt @@ -223,7 +223,7 @@ internal class TokenDetailsOnrampTransactionStateConverter( this == OnrampStatus.Status.RefundInProgress || this == OnrampStatus.Status.Refunded -> ExpressStatusItemState.Error else -> getStatusState(OnrampStatus.Status.Paid) - } + }, ) private fun OnrampStatus.Status.getSendingItem() = ExpressStatusItemUM( @@ -255,7 +255,7 @@ internal class TokenDetailsOnrampTransactionStateConverter( }, state = when { this == OnrampStatus.Status.RefundInProgress -> ExpressStatusItemState.Active - this == OnrampStatus.Status.Refunded -> ExpressStatusItemState.Error + this == OnrampStatus.Status.Refunded -> ExpressStatusItemState.Done else -> getStatusState(OnrampStatus.Status.Sending) }, ) From 6f8770007e105d96527d9e04fb864ca97138a6c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 May 2025 20:27:07 +0400 Subject: [PATCH 04/19] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/TangemApplication.kt | 2 ++ .../java/com/tangem/tap/data/DefaultCardSdkProvider.kt | 6 ++++-- .../com/tangem/datasource/api/common/config/ApiConfig.kt | 4 ++-- .../common/config/{TangemCardSdk.kt => Attestation.kt} | 4 ++-- .../java/com/tangem/datasource/di/ApiConfigsModule.kt | 2 +- .../tangem/datasource/utils/NetworkLogsSaveInterceptor.kt | 2 +- .../common/config/managers/ProdApiConfigsManagerTest.kt | 6 +++--- .../tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt | 7 ++++++- .../viewmodels/EnvironmentsTogglesViewModel.kt | 8 ++++---- 9 files changed, 25 insertions(+), 16 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/api/common/config/{TangemCardSdk.kt => Attestation.kt} (87%) diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index d61bb4e130..ed0018b6d9 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -37,6 +37,7 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.utils.NetworkLogsSaveInterceptor import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -311,6 +312,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat TangemApiServiceLogging.addInterceptors( createNetworkLoggingInterceptor(), ChuckerInterceptor(this), + NetworkLogsSaveInterceptor(appLogsStore), ) } diff --git a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt index b95fdcbadf..b162d5f389 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt @@ -32,6 +32,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.wallet.BuildConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -64,7 +65,7 @@ internal class DefaultCardSdkProvider @Inject constructor( if (BuildConfig.TESTER_MENU_ENABLED) { appPreferencesStore.getObjectMap(PreferencesKeys.apiConfigsEnvironmentKey) .map { - when (it[ApiConfig.ID.TangemCardSdk.name]) { + when (it[ApiConfig.ID.Attestation.name]) { ApiEnvironment.DEV, ApiEnvironment.STAGE, -> false @@ -73,6 +74,7 @@ internal class DefaultCardSdkProvider @Inject constructor( -> true } } + .distinctUntilChanged() .onEach { isProd -> holder?.let { it.sdk.config.isTangemAttestationProdEnv = isProd @@ -170,7 +172,7 @@ internal class DefaultCardSdkProvider @Inject constructor( config = config.apply { isNewOnlineAttestationEnabled = cardSdkFeatureToggles.isNewAttestationEnabled - val apiConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemCardSdk) + val apiConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.Attestation) isTangemAttestationProdEnv = apiConfig.environment == ApiEnvironment.PROD }, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index b6bc222e76..f3cc288b25 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -25,7 +25,7 @@ sealed class ApiConfig { TangemTech, StakeKit, TangemVisa, - TangemCardSdk, + Attestation, BlockAid, } @@ -35,7 +35,7 @@ sealed class ApiConfig { is TangemTech -> ID.TangemTech is StakeKit -> ID.StakeKit is TangemVisa -> ID.TangemVisa - is TangemCardSdk -> ID.TangemCardSdk + is Attestation -> ID.Attestation is BlockAid -> ID.BlockAid } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemCardSdk.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Attestation.kt similarity index 87% rename from core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemCardSdk.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/config/Attestation.kt index 49edb4a7d8..f3d0af5272 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemCardSdk.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Attestation.kt @@ -1,11 +1,11 @@ package com.tangem.datasource.api.common.config /** - * Tangem API [ApiConfig] that used in CardSDK + * Attestation [ApiConfig] that used in CardSDK * [REDACTED_AUTHOR] */ -internal class TangemCardSdk : ApiConfig() { +internal class Attestation : ApiConfig() { override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index 62107738e3..174cf88a63 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -49,5 +49,5 @@ internal object ApiConfigsModule { @Provides @IntoSet - fun provideTangemCardSdkConfig(): ApiConfig = TangemCardSdk() + fun provideAttestationConfig(): ApiConfig = Attestation() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt index 26703087a9..958c875e21 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt @@ -25,7 +25,7 @@ private const val JSON_INDENT_SPACES = 4 * [REDACTED_AUTHOR] */ -internal class NetworkLogsSaveInterceptor( +class NetworkLogsSaveInterceptor( private val appLogsStore: AppLogsStore, ) : Interceptor { diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 116ddea237..3167db2d61 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -86,7 +86,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { is TangemTech -> createTangemTechModel() is StakeKit -> createStakeKitModel() is TangemVisa -> createVisaModel() - is TangemCardSdk -> createTangemCardSdkModel() + is Attestation -> createAttestationModel() is BlockAid -> createBlockAidSdkModel() } } @@ -189,9 +189,9 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { ) } - private fun createTangemCardSdkModel(): Model { + private fun createAttestationModel(): Model { return Model( - id = ApiConfig.ID.TangemCardSdk, + id = ApiConfig.ID.Attestation, expected = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, baseUrl = "https://api.tangem-tech.com/", diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt index e9fa631d23..53377c3a3e 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt @@ -10,6 +10,7 @@ import com.tangem.domain.core.utils.catchOn import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -53,7 +54,11 @@ internal class DefaultMultiQuoteFetcher @Inject constructor( val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",") val response = safeApiCallWithTimeout( - call = { tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds).bind() }, + call = { + withContext(dispatchers.io) { + tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds).bind() + } + }, onError = { error -> throw error }, ) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt index 3777d8a26a..0da6b14112 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt @@ -96,15 +96,15 @@ internal class EnvironmentsTogglesViewModel @Inject constructor( viewModelScope.launch { val environment = ApiEnvironment.valueOf(name) - handleIfTangemCardSdkConfig(id = id, environment = environment) + handleIfAttestationConfig(id = id, environment = environment) mutableApiConfigsManager.changeEnvironment(id = id, environment = environment) } } - /** Special logic for [ApiConfig.ID.TangemCardSdk] */ - private fun handleIfTangemCardSdkConfig(id: String, environment: ApiEnvironment) { - if (id == ApiConfig.ID.TangemCardSdk.name) { + /** Special logic for [ApiConfig.ID.Attestation] */ + private fun handleIfAttestationConfig(id: String, environment: ApiEnvironment) { + if (id == ApiConfig.ID.Attestation.name) { cardSdkConfigRepository.setTangemApiProdEnvFlag( flag = when (environment) { ApiEnvironment.PROD -> true From 5c89ba7be86594c5b751cb3302e868b8190f8fef Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 9 May 2025 18:15:04 +0700 Subject: [PATCH 05/19] Updated on 2026-08-14 --- .../main/java/com/tangem/blockchainsdk/utils/Blockchain.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 484b6f89ee..39409aaddb 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -427,8 +427,8 @@ fun Blockchain.toCoinId(): String { Blockchain.Bitrock, Blockchain.BitrockTestnet -> "bitrock" Blockchain.Sonic, Blockchain.SonicTestnet -> "sonic-3" Blockchain.ApeChain, Blockchain.ApeChainTestnet -> "apecoin" - Blockchain.Scroll, Blockchain.ScrollTestnet -> "scroll" - Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet -> "zklink" + Blockchain.Scroll, Blockchain.ScrollTestnet -> "scroll-ethereum" + Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet -> "zklink-ethereum" Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> "pepecoin-network" } } From fc04689bc21a5ceca976148ae2c44a683afcbcec Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 May 2025 18:53:21 +0300 Subject: [PATCH 06/19] Updated on 2026-08-14 --- .../factory/amount/OnrampAmountStateFactory.kt | 8 ++++++-- .../onramp/main/model/OnrampMainComponentModel.kt | 13 ++++++++++++- .../onramp/providers/model/SelectProviderModel.kt | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt index b9a666cb86..cf04d7b7e8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt @@ -26,6 +26,7 @@ internal class OnrampAmountStateFactory( private val analyticsEventHandler: AnalyticsEventHandler, private val onrampIntents: OnrampIntents, private val cryptoCurrency: CryptoCurrency, + private val needApplyFCARestrictions: Provider, ) { private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter( @@ -122,7 +123,10 @@ internal class OnrampAmountStateFactory( .filterNot { it == bestProvider } .any { it is OnrampQuote.Data } - val isBestProvider = selectedQuote == bestProvider && isMultipleQuotes && isOtherQuotesHasData + val isBestProvider = selectedQuote == bestProvider && + isMultipleQuotes && + isOtherQuotesHasData && + !needApplyFCARestrictions() return currentState.copy( providerBlockState = selectedQuote.toProviderBlockState(isBestProvider), @@ -154,7 +158,7 @@ internal class OnrampAmountStateFactory( paymentMethod = providerResult.paymentMethod, providerId = providerResult.provider.id, providerName = providerResult.provider.info.name, - isBestRate = isBestRate, + isBestRate = isBestRate && !needApplyFCARestrictions(), onClick = onrampIntents::openProviders, termsOfUseLink = providerResult.provider.info.termsOfUseLink, privacyPolicyLink = providerResult.provider.info.privacyPolicyLink, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 09feb75a0d..5ea7c807a7 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -20,6 +20,9 @@ import com.tangem.domain.onramp.model.OnrampAvailability import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.onramp.model.error.OnrampError +import com.tangem.domain.settings.usercountry.GetUserCountryUseCase +import com.tangem.domain.settings.usercountry.models.UserCountry +import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.main.OnrampMainComponent import com.tangem.features.onramp.main.entity.* @@ -35,6 +38,7 @@ import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +import java.util.Locale import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -53,6 +57,7 @@ internal class OnrampMainComponentModel @Inject constructor( private val messageSender: UiMessageSender, private val urlOpener: UrlOpener, getWalletsUseCase: GetWalletsUseCase, + getUserCountryUseCase: GetUserCountryUseCase, paramsContainer: ParamsContainer, ) : Model(), OnrampIntents { @@ -70,6 +75,7 @@ internal class OnrampMainComponentModel @Inject constructor( analyticsEventHandler = analyticsEventHandler, onrampIntents = this, cryptoCurrency = params.cryptoCurrency, + needApplyFCARestrictions = Provider { userCountry.needApplyFCARestrictions() }, ) private val _state: MutableStateFlow = MutableStateFlow( value = stateFactory.getInitialState( @@ -82,8 +88,12 @@ internal class OnrampMainComponentModel @Inject constructor( val bottomSheetNavigation: SlotNavigation = SlotNavigation() private val lastUpdateState = mutableStateOf(null) + private var userCountry: UserCountry? = null init { + userCountry = getUserCountryUseCase.invokeSync().getOrNull() + ?: UserCountry.Other(Locale.getDefault().country) + sendScreenOpenAnalytics() checkResidenceCountry() subscribeToAmountChanges() @@ -387,7 +397,8 @@ internal class OnrampMainComponentModel @Inject constructor( providerName = errorState.provider.info.name, paymentMethod = errorState.paymentMethod.name, ) - else -> { /* no-op */ } + else -> { /* no-op */ + } } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt index 6a367639cb..0cc48e4657 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt @@ -52,7 +52,7 @@ internal class SelectProviderModel @Inject constructor( private val getOnrampSelectedPaymentMethodUseCase: GetOnrampSelectedPaymentMethodUseCase, private val getOnrampProviderWithQuoteUseCase: GetOnrampProviderWithQuoteUseCase, private val saveSelectedPaymentMethod: OnrampSaveSelectedPaymentMethod, - private val getUserCountryUseCase: GetUserCountryUseCase, + getUserCountryUseCase: GetUserCountryUseCase, paramsContainer: ParamsContainer, ) : Model() { From 631312eb3fa1383814d96d996e1ee1f4b23a866d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 13:46:48 +0500 Subject: [PATCH 07/19] Updated on 2026-08-14 --- .../model/analytics/TokenOnrampAnalyticsEvent.kt | 10 +++++----- .../conterter/SetOnrampSuccessContentConverter.kt | 2 +- .../success/model/OnrampSuccessComponentModel.kt | 2 ++ .../TokenDetailsOnrampTransactionStateConverter.kt | 6 +++++- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt index ef07a4082d..ffa78172b9 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt @@ -5,12 +5,12 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER import com.tangem.core.analytics.models.AnalyticsParam.Key.STATUS import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM -class TokenOnrampAnalyticsEvent( +sealed class TokenOnrampAnalyticsEvent( event: String, params: Map = mapOf(), ) : AnalyticsEvent("Onramp", event, params) { - class OnrampStatusOpened(tokenSymbol: String, provider: String, fiatCurrency: String) : TokenScreenAnalyticsEvent( + class OnrampStatusOpened(tokenSymbol: String, provider: String, fiatCurrency: String) : TokenOnrampAnalyticsEvent( event = "Onramp Status Opened", params = mapOf( TOKEN_PARAM to tokenSymbol, @@ -20,7 +20,7 @@ class TokenOnrampAnalyticsEvent( ) class OnrampStatusChanged(tokenSymbol: String, status: String, provider: String, fiatCurrency: String) : - TokenScreenAnalyticsEvent( + TokenOnrampAnalyticsEvent( event = "Onramp Status", params = mapOf( TOKEN_PARAM to tokenSymbol, @@ -30,12 +30,12 @@ class TokenOnrampAnalyticsEvent( ), ) - data object GoToProvider : TokenScreenAnalyticsEvent(event = "Button - Go To Provider") + data object GoToProvider : TokenOnrampAnalyticsEvent(event = "Button - Go To Provider") data class NoticeKYC( val tokenSymbol: String, val provider: String, - ) : TokenScreenAnalyticsEvent( + ) : TokenOnrampAnalyticsEvent( event = "Notice - KYC", params = mapOf( TOKEN_PARAM to tokenSymbol, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt index b5d275dac8..ab7e4b6f85 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt @@ -213,7 +213,7 @@ internal class SetOnrampSuccessContentConverter( else -> if (externalTxId != null) { ExpressLinkUM.Content( icon = R.drawable.ic_copy_24, - text = resourceReference(R.string.express_transaction_id, wrappedList(externalTxId)), + text = stringReference(externalTxId), onClick = { onCopyClick(externalTxId) }, ) } else { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 80caf88f09..436bcd0791 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -20,6 +20,7 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.tokens.GetCryptoCurrencyUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.onramp.component.OnrampSuccessComponent import com.tangem.features.onramp.impl.R @@ -64,6 +65,7 @@ internal class OnrampSuccessComponentModel @Inject constructor( } override fun goToProviderClick(providerLink: String) { + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) urlOpener.openUrl(providerLink) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt index 06644ff6e6..44970bac5f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt @@ -116,7 +116,10 @@ internal class TokenDetailsOnrampTransactionStateConverter( } private fun onProviderClick(externalTxUrl: String?) = if (externalTxUrl != null) { - { clickIntents.onGoToProviderClick(externalTxUrl) } + { + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) + clickIntents.onGoToProviderClick(externalTxUrl) + } } else { null } @@ -270,6 +273,7 @@ internal class TokenDetailsOnrampTransactionStateConverter( icon = R.drawable.ic_arrow_top_right_24, text = resourceReference(R.string.common_go_to_provider), onClick = { + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) clickIntents.onGoToProviderClick(externalTxUrl) }, ) From 34d8f4bf460c1e92e456d52f9d1bb86720889f97 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 13:47:08 +0500 Subject: [PATCH 08/19] Updated on 2026-08-14 --- .../main/model/OnrampMainComponentModel.kt | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 5ea7c807a7..12371bbef8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -297,17 +297,6 @@ internal class OnrampMainComponentModel @Inject constructor( lastUpdateState.value = null return } - - if (checkLastInputState(quote)) { - lastUpdateState.value = OnrampLastUpdate( - quote.fromAmount, - quote.countryCode, - ) - - _state.update { - amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes) - } - } _state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) } } @@ -322,7 +311,7 @@ internal class OnrampMainComponentModel @Inject constructor( val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error } // Check if amount, country or currency has changed - return if (checkLastInputState(quoteToCheck)) { + val newQuote = if (checkLastInputState(quoteToCheck)) { quoteToCheck } else { val state = state.value as? OnrampMainComponentUM.Content @@ -341,6 +330,20 @@ internal class OnrampMainComponentModel @Inject constructor( lastSelectedQuote } } + newQuote?.let { updateProvider(newQuote, quotes) } + + return newQuote + } + + private fun updateProvider(quote: OnrampQuote, quotes: List) { + lastUpdateState.value = OnrampLastUpdate( + quote.fromAmount, + quote.countryCode, + ) + + _state.update { + amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes) + } } private fun onRetryQuotes() { From c12a928e72f553dbd03c7580c5684c0ca397e103 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 14:08:30 +0500 Subject: [PATCH 09/19] Updated on 2026-08-14 --- .../datasource/api/express/TangemExpressApi.kt | 14 +++++++------- .../com/tangem/datasource/api/onramp/OnrampApi.kt | 14 +++++++------- .../exchangeservice/swap/ExpressUtils.kt | 4 ++-- .../tangem/data/onramp/DefaultOnrampRepository.kt | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 469ab8a516..a57712606b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -16,27 +16,27 @@ interface TangemExpressApi { @POST("assets") suspend fun getAssets( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, @Body body: AssetsRequestBody, ): ApiResponse> @POST("pairs") suspend fun getPairs( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, @Body body: PairsRequestBody, ): ApiResponse> @GET("providers") suspend fun getProviders( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, ): ApiResponse> @GET("exchange-quote") suspend fun getExchangeQuote( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, @Query("fromContractAddress") fromContractAddress: String, @Query("fromNetwork") fromNetwork: String, @Query("toContractAddress") toContractAddress: String, @@ -51,7 +51,7 @@ interface TangemExpressApi { @GET("exchange-data") suspend fun getExchangeData( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, @Query("fromContractAddress") fromContractAddress: String, @Query("fromNetwork") fromNetwork: String, @Query("toContractAddress") toContractAddress: String, @@ -71,14 +71,14 @@ interface TangemExpressApi { @GET("exchange-status") suspend fun getExchangeStatus( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, @Query("txId") txId: String, ): ApiResponse @POST("exchange-sent") suspend fun exchangeSent( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, @Body body: ExchangeSentRequestBody, ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt index 360e713a62..01d636242a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt @@ -17,38 +17,38 @@ interface OnrampApi { @GET("currencies") suspend fun getCurrencies( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, ): ApiResponse> @GET("countries") suspend fun getCountries( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, ): ApiResponse> @GET("country-by-ip") suspend fun getCountryByIp( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, ): ApiResponse @GET("payment-methods") suspend fun getPaymentMethods( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, ): ApiResponse> @POST("onramp-pairs") suspend fun getPairs( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, @Body body: OnrampPairsRequest, ): ApiResponse> @GET("onramp-quote") suspend fun getQuote( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, @Query("fromCurrencyCode") fromCurrencyCode: String, @Query("fromPrecision") fromPrecision: Int, @Query("toContractAddress") toContractAddress: String, @@ -83,7 +83,7 @@ interface OnrampApi { @GET("onramp-status") suspend fun getStatus( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, @Query("txId") txId: String, ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressUtils.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressUtils.kt index 50d0877467..d1176e7256 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressUtils.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressUtils.kt @@ -11,12 +11,12 @@ object ExpressUtils { private const val BATCH_ID_CHANGENOW = "BB000013" private const val BATCH_ID_PARTNER = "AF990015" - fun getRefCode(userWallet: UserWallet, appPreferencesStore: AppPreferencesStore): String { + fun getRefCode(userWallet: UserWallet, appPreferencesStore: AppPreferencesStore): String? { return when { isRing(userWallet, appPreferencesStore) -> "ring" isChangeNow(userWallet) -> "ChangeNow" isPartner(userWallet) -> "partner" - else -> "" + else -> null } } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index b587d6f7e4..0260a3f859 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -372,7 +372,7 @@ internal class DefaultOnrampRepository( refCode = ExpressUtils.getRefCode( userWallet = userWallet, appPreferencesStore = appPreferencesStore, - ).takeUnless { it.isEmpty() }, + ), ).bind() }, onError = { e -> From 90c64f89823111704e1411611fb0d45c2fe2ac50 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 11:54:14 +0400 Subject: [PATCH 10/19] Updated on 2026-08-14 --- .../multi/DefaultMultiNetworkStatusFetcher.kt | 82 +++++- .../DefaultSingleNetworkStatusFetcher.kt | 14 +- .../store/DefaultNetworksStatusesStoreV2.kt | 11 + .../networks/store/NetworksStatusesStoreV2.kt | 3 + .../DefaultMultiNetworkStatusFetcherTest.kt | 242 +++++++++--------- .../DefaultSingleNetworkStatusFetcherTest.kt | 41 +-- .../NetworkStatusesStoreUpdateMethodsTest.kt | 45 ++++ .../com/tangem/domain/core/utils/EitherExt.kt | 14 + .../single/SingleNetworkStatusFetcher.kt | 27 +- .../tokens/FetchCurrencyStatusUseCase.kt | 2 +- .../UpdateDelayedNetworkStatusUseCase.kt | 2 +- .../usecase/SendTransactionUseCase.kt | 2 +- 12 files changed, 292 insertions(+), 193 deletions(-) diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt index c807f56761..cd67ca7880 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -1,11 +1,27 @@ package com.tangem.data.networks.multi -import arrow.core.Either -import arrow.core.raise.either +import arrow.core.raise.catch import arrow.core.raise.ensure +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.core.utils.eitherOn +import com.tangem.domain.demo.DemoConfig import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -20,25 +36,52 @@ import javax.inject.Inject [REDACTED_AUTHOR] */ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( - private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + excludedBlockchains: ExcludedBlockchains, private val networksStatusesStore: NetworksStatusesStoreV2, + private val userWalletsStore: UserWalletsStore, + private val appPreferencesStore: AppPreferencesStore, + private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + private val dispatchers: CoroutineDispatcherProvider, ) : MultiNetworkStatusFetcher { - override suspend fun invoke(params: MultiNetworkStatusFetcher.Params): Either = either { + private val demoConfig = DemoConfig() + private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains) + private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory(excludedBlockchains) } + + override suspend fun invoke(params: MultiNetworkStatusFetcher.Params) = eitherOn(dispatchers.default) { // Optimization! // Every singleNetworkStatusFetcher with applyRefresh as true will refresh every network in the store. // So if we update all networks at once, it will be more efficient. networksStatusesStore.refresh(userWalletId = params.userWalletId, networks = params.networks) + val userWallet = catch( + block = { userWalletsStore.getSyncStrict(key = params.userWalletId) }, + catch = { + networksStatusesStore.storeError(userWalletId = params.userWalletId, networks = params.networks) + raise(it) + }, + ) + + val isNotSingleWallet = with(userWallet.scanResponse.cardTypesResolver) { + isMultiwalletAllowed() || isSingleWalletWithToken() + } + + ensure(isNotSingleWallet) { + networksStatusesStore.storeError(userWalletId = params.userWalletId, networks = params.networks) + IllegalStateException("User wallet is not multi-currency") + } + + val networksCurrencies = createCurrencies(userWallet = userWallet, networks = params.networks) + val result = coroutineScope { params.networks .map { async { singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params( + params = SingleNetworkStatusFetcher.Params.Prepared( userWalletId = params.userWalletId, network = it, - applyRefresh = false, + addedNetworkCurrencies = networksCurrencies, ), ) } @@ -52,4 +95,31 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( IllegalStateException("Failed to fetch network statuses") } } + + private suspend fun createCurrencies(userWallet: UserWallet, networks: Set): Set { + val blockchains = networks.map { Blockchain.fromNetworkId(networkId = it.backendId) } + + // multi-currency wallet + if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, networks = networks) + + // check if the blockchain of single-currency wallet is the same as network + val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() + if (!blockchains.contains(cardBlockchain)) return emptySet() + + return cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse).toSet() + } + + private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, networks: Set): Set { + val response = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue), + ) ?: return emptySet() + + return responseCurrenciesFactory.createCurrencies( + tokens = response.tokens.filter { token -> + networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath } + }, + scanResponse = userWallet.scanResponse, + ) + .toSet() + } } \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt index 6f72de7745..9d071fe5f3 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt @@ -54,12 +54,16 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( private val networkStatusFactory = NetworkStatusFactory() override suspend fun invoke(params: SingleNetworkStatusFetcher.Params) = Either.catchOn(dispatchers.default) { - if (params.applyRefresh) { - networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network) - } + val networkCurrencies = when (params) { + is SingleNetworkStatusFetcher.Params.Prepared -> params.addedNetworkCurrencies + is SingleNetworkStatusFetcher.Params.Simple -> { + networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network) - val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) - val networkCurrencies = createCurrencies(userWallet = userWallet, network = params.network) + val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) + + createCurrencies(userWallet = userWallet, network = params.network) + } + } val result = withContext(dispatchers.io) { walletManagersFacade.update( diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt index e9790f1fd1..5e28c87b64 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt @@ -105,6 +105,17 @@ internal class DefaultNetworksStatusesStoreV2( ) } + override suspend fun storeError(userWalletId: UserWalletId, networks: Set) { + updateInRuntime( + userWalletId = userWalletId, + networks = networks, + ifNotFound = ::createUnreachableStatus, + update = { status -> + status.copy(value = status.value.copySealed(source = StatusSource.ONLY_CACHE)) + }, + ) + } + private suspend fun updateInRuntime( userWalletId: UserWalletId, networks: Set, diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt index d3b9e7b595..b0a421623c 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt @@ -27,4 +27,7 @@ internal interface NetworksStatusesStoreV2 { * If [value] is null, default unreachable status will be stored. */ suspend fun storeError(userWalletId: UserWalletId, network: Network, value: NetworkStatus.Unreachable? = null) + + /** Store error for [networks] by [userWalletId] */ + suspend fun storeError(userWalletId: UserWalletId, networks: Set) } \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt index ab79da0dd1..18ea008ac3 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt @@ -1,133 +1,119 @@ package com.tangem.data.networks.multi -import arrow.core.left -import arrow.core.right -import com.google.common.truth.Truth -import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory -import com.tangem.data.networks.store.NetworksStatusesStoreV2 -import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.networks.single.SingleNetworkStatusFetcher -import com.tangem.domain.wallets.models.UserWalletId -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.Test - /** [REDACTED_AUTHOR] */ -internal class DefaultMultiNetworkStatusFetcherTest { - - private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk() - private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true) - - private val fetcher = DefaultMultiNetworkStatusFetcher( - singleNetworkStatusFetcher = singleNetworkStatusFetcher, - networksStatusesStore = networksStatusesStore, - ) - - @Test - fun `fetch networks statuses successfully`() = runTest { - val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) - - val ethParams = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = ethereumAndStellar.first(), - applyRefresh = false, - ) - - val stellarParams = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = ethereumAndStellar.last(), - applyRefresh = false, - ) - - coEvery { singleNetworkStatusFetcher(ethParams) } returns Unit.right() - coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() - - val actual = fetcher(params) - - coVerify { - networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) - singleNetworkStatusFetcher(ethParams) - singleNetworkStatusFetcher(stellarParams) - } - - Truth.assertThat(actual.isRight()).isTrue() - } - - @Test - fun `fetch networks statuses failure if one of them fails`() = runTest { - val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) - - val ethParams = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = ethereumAndStellar.first(), - applyRefresh = false, - ) - - val stellarParams = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = ethereumAndStellar.last(), - applyRefresh = false, - ) - - val ethException = IllegalStateException("eth") - coEvery { singleNetworkStatusFetcher(ethParams) } returns ethException.left() - coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() - - val actual = fetcher(params) - - coVerify { - networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) - singleNetworkStatusFetcher(ethParams) - singleNetworkStatusFetcher(stellarParams) - } - - val expected = IllegalStateException("Failed to fetch network statuses") - - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) - } - - @Test - fun `fetch networks statuses failure if all of them fails`() = runTest { - val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) - - val ethParams = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = ethereumAndStellar.first(), - applyRefresh = false, - ) - - val stellarParams = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = ethereumAndStellar.last(), - applyRefresh = false, - ) - - coEvery { singleNetworkStatusFetcher(ethParams) } returns IllegalStateException("eth").left() - coEvery { singleNetworkStatusFetcher(stellarParams) } returns IllegalStateException("stellar").left() - - val actual = fetcher(params) - - coVerify { - networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) - singleNetworkStatusFetcher(ethParams) - singleNetworkStatusFetcher(stellarParams) - } - - val expected = IllegalStateException("Failed to fetch network statuses") - - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) - } - - private companion object { - val userWalletId = UserWalletId("011") - val ethereumAndStellar = MockCryptoCurrencyFactory().ethereumAndStellar.map { it.network }.toSet() - } -} \ No newline at end of file +// internal class DefaultMultiNetworkStatusFetcherTest { +// +// private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk() +// private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true) +// +// private val fetcher = DefaultMultiNetworkStatusFetcher( +// singleNetworkStatusFetcher = singleNetworkStatusFetcher, +// networksStatusesStore = networksStatusesStore, +// ) +// +// @Test +// fun `fetch networks statuses successfully`() = runTest { +// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) +// +// val ethParams = SingleNetworkStatusFetcher.Params( +// userWalletId = userWalletId, +// network = ethereumAndStellar.first(), +// applyRefresh = false, +// ) +// +// val stellarParams = SingleNetworkStatusFetcher.Params( +// userWalletId = userWalletId, +// network = ethereumAndStellar.last(), +// applyRefresh = false, +// ) +// +// coEvery { singleNetworkStatusFetcher(ethParams) } returns Unit.right() +// coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() +// +// val actual = fetcher(params) +// +// coVerify { +// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) +// singleNetworkStatusFetcher(ethParams) +// singleNetworkStatusFetcher(stellarParams) +// } +// +// Truth.assertThat(actual.isRight()).isTrue() +// } +// +// @Test +// fun `fetch networks statuses failure if one of them fails`() = runTest { +// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) +// +// val ethParams = SingleNetworkStatusFetcher.Params( +// userWalletId = userWalletId, +// network = ethereumAndStellar.first(), +// applyRefresh = false, +// ) +// +// val stellarParams = SingleNetworkStatusFetcher.Params( +// userWalletId = userWalletId, +// network = ethereumAndStellar.last(), +// applyRefresh = false, +// ) +// +// val ethException = IllegalStateException("eth") +// coEvery { singleNetworkStatusFetcher(ethParams) } returns ethException.left() +// coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() +// +// val actual = fetcher(params) +// +// coVerify { +// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) +// singleNetworkStatusFetcher(ethParams) +// singleNetworkStatusFetcher(stellarParams) +// } +// +// val expected = IllegalStateException("Failed to fetch network statuses") +// +// Truth.assertThat(actual.isLeft()).isTrue() +// Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) +// Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) +// } +// +// @Test +// fun `fetch networks statuses failure if all of them fails`() = runTest { +// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) +// +// val ethParams = SingleNetworkStatusFetcher.Params( +// userWalletId = userWalletId, +// network = ethereumAndStellar.first(), +// applyRefresh = false, +// ) +// +// val stellarParams = SingleNetworkStatusFetcher.Params( +// userWalletId = userWalletId, +// network = ethereumAndStellar.last(), +// applyRefresh = false, +// ) +// +// coEvery { singleNetworkStatusFetcher(ethParams) } returns IllegalStateException("eth").left() +// coEvery { singleNetworkStatusFetcher(stellarParams) } returns IllegalStateException("stellar").left() +// +// val actual = fetcher(params) +// +// coVerify { +// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) +// singleNetworkStatusFetcher(ethParams) +// singleNetworkStatusFetcher(stellarParams) +// } +// +// val expected = IllegalStateException("Failed to fetch network statuses") +// +// Truth.assertThat(actual.isLeft()).isTrue() +// Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) +// Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) +// } +// +// private companion object { +// val userWalletId = UserWalletId("011") +// val ethereumAndStellar = MockCryptoCurrencyFactory().ethereumAndStellar.map { it.network }.toSet() +// } +// } \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt index 7f2a925235..7ae26a8b31 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt @@ -37,11 +37,7 @@ internal class DefaultSingleNetworkStatusFetcherTest { @Test fun `fetch network status successfully`() = runTest { - val params = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = network, - applyRefresh = true, - ) + val params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network) val result = UpdateWalletManagerResult.MissedDerivation coEvery { walletManagersFacade.update(userWalletId, network, emptySet()) } returns result @@ -63,11 +59,7 @@ internal class DefaultSingleNetworkStatusFetcherTest { @Test fun `fetch network status failure`() = runTest { - val params = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = network, - applyRefresh = true, - ) + val params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network) val exception = IllegalStateException() coEvery { userWalletsStore.getSyncStrict(key = userWalletId) } throws exception @@ -89,35 +81,6 @@ internal class DefaultSingleNetworkStatusFetcherTest { Truth.assertThat(actual.leftOrNull()).isEqualTo(exception) } - @Test - fun `fetch network status if applyRefresh is false`() = runTest { - val params = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = network, - applyRefresh = false, - ) - - val result = UpdateWalletManagerResult.MissedDerivation - coEvery { walletManagersFacade.update(userWalletId, network, emptySet()) } returns result - - val actual = fetcher(params) - - coVerifyOrder { - userWalletsStore.getSyncStrict(key = userWalletId) - walletManagersFacade.update(userWalletId, network, emptySet()) - networksStatusesStore.storeSuccess( - userWalletId = userWalletId, - value = NetworkStatus(network, NetworkStatus.MissedDerivation), - ) - } - - coVerify(inverse = true) { - networksStatusesStore.refresh(userWalletId = any(), network = any()) - } - - Truth.assertThat(actual.isRight()).isTrue() - } - private companion object { val userWalletId = UserWalletId("011") val network = MockCryptoCurrencyFactory().ethereum.network diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt index ebf07bff35..d1f2080794 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt @@ -280,6 +280,51 @@ internal class NetworkStatusesStoreUpdateMethodsTest { Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) } + @Test + fun `store error for networks if runtime store is empty`() = runTest { + store.storeError(userWalletId = userWalletId, networks = networks) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + SimpleNetworkStatus( + id = SimpleNetworkStatus.Id(networks.first()), + value = NetworkStatus.Unreachable(address = null), + ), + SimpleNetworkStatus( + id = SimpleNetworkStatus.Id(networks.last()), + value = NetworkStatus.Unreachable(address = null), + ), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `store error for networks if runtime store contains status with this network`() = runTest { + val status = MockNetworkStatusFactory.createVerified(networks.first()).toSimple() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(status)), + ) + + store.storeError(userWalletId = userWalletId, networks = networks) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + status.copy(value = status.value.copySealed(source = StatusSource.ONLY_CACHE)), + SimpleNetworkStatus( + id = SimpleNetworkStatus.Id(networks.last()), + value = NetworkStatus.Unreachable(address = null), + ), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + private companion object { val userWalletId = UserWalletId(stringValue = "011") diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/utils/EitherExt.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/utils/EitherExt.kt index b961210633..04a4819dac 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/utils/EitherExt.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/utils/EitherExt.kt @@ -1,10 +1,13 @@ package com.tangem.domain.core.utils import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.either import com.tangem.domain.core.lce.Lce import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext +import kotlin.experimental.ExperimentalTypeInference /** * [Flow] of [Either] @@ -47,4 +50,15 @@ suspend inline fun Either.Companion.catchOn( context = dispatcher, block = { catch { function() } }, ) +} + +@JvmName("eitherWithDispatch") +@OptIn(ExperimentalTypeInference::class) +suspend inline fun eitherOn( + dispatcher: CoroutineDispatcher, + @BuilderInference noinline block: suspend Raise.() -> A, +): Either { + return withContext(dispatcher) { + either { block() } + } } \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt index d9ab0fbece..30836055f1 100644 --- a/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt +++ b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt @@ -1,6 +1,7 @@ package com.tangem.domain.networks.single import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -11,16 +12,18 @@ import com.tangem.domain.wallets.models.UserWalletId */ interface SingleNetworkStatusFetcher : FlowFetcher { - /** - * Params - * - * @property userWalletId wallet id - * @property network network - * @property applyRefresh flag that determines whether to apply refresh (see DefaultMultiNetworkStatusFetcher) - */ - data class Params( - val userWalletId: UserWalletId, - val network: Network, - val applyRefresh: Boolean = true, - ) + /** Params */ + sealed interface Params { + + val userWalletId: UserWalletId + val network: Network + + data class Simple(override val userWalletId: UserWalletId, override val network: Network) : Params + + data class Prepared( + override val userWalletId: UserWalletId, + override val network: Network, + val addedNetworkCurrencies: Set, + ) : Params + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index e9775c5d32..fd282510f7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -127,7 +127,7 @@ class FetchCurrencyStatusUseCase( ) { if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network), + params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network), ) .mapLeft { CurrencyStatusError.DataError(it) } } else { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt index 5be8ae95ef..6626764234 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt @@ -52,7 +52,7 @@ class UpdateDelayedNetworkStatusUseCase( ) { if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network), + params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network), ) .mapLeft { CurrencyStatusError.DataError(it) } } else { diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 15efbc7500..08c37d0043 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -84,7 +84,7 @@ class SendTransactionUseCase( .onRight { if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params( + params = SingleNetworkStatusFetcher.Params.Simple( userWalletId = userWallet.walletId, network = network, ), From 3bca437ea440d2ec5d9f85f082df8dfe694b05eb Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 17:40:19 +0400 Subject: [PATCH 11/19] Updated on 2026-08-14 --- .../single/DefaultSingleNetworkStatusFetcher.kt | 13 +++++++++++-- .../store/DefaultNetworksStatusesStoreV2.kt | 17 +++++++++++++++++ .../networks/store/NetworksStatusesStoreV2.kt | 6 ++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt index 9d071fe5f3..4d2247b73e 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt @@ -83,11 +83,20 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( val statusValue = status.value if (statusValue is NetworkStatus.Unreachable) { - networksStatusesStore.storeError( + val prevStatus = networksStatusesStore.getSyncOrNull( userWalletId = params.userWalletId, network = params.network, - value = statusValue, ) + + if (prevStatus?.value is NetworkStatus.MissedDerivation) { + networksStatusesStore.storeUnreachableStatus(userWalletId = params.userWalletId, value = status) + } else { + networksStatusesStore.storeError( + userWalletId = params.userWalletId, + network = params.network, + value = statusValue, + ) + } } else { networksStatusesStore.storeSuccess(userWalletId = params.userWalletId, value = status) } diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt index 5e28c87b64..943e6d1400 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt @@ -55,6 +55,12 @@ internal class DefaultNetworksStatusesStoreV2( return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] } } + override suspend fun getSyncOrNull(userWalletId: UserWalletId, network: Network): SimpleNetworkStatus? { + val simpleStatusId = SimpleNetworkStatus.Id(network = network) + + return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue)?.firstOrNull { it.id == simpleStatusId } + } + override suspend fun refresh(userWalletId: UserWalletId, network: Network) { refresh(userWalletId = userWalletId, networks = setOf(network)) } @@ -116,6 +122,17 @@ internal class DefaultNetworksStatusesStoreV2( ) } + override suspend fun storeUnreachableStatus(userWalletId: UserWalletId, value: NetworkStatus) { + if (value.value !is NetworkStatus.Unreachable) { + val message = "Use storeError method to save unreachable status" + Timber.d(message) + + error(message) + } + + storeInRuntime(userWalletId = userWalletId, status = value) + } + private suspend fun updateInRuntime( userWalletId: UserWalletId, networks: Set, diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt index b0a421623c..fb40156849 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt @@ -12,6 +12,9 @@ internal interface NetworksStatusesStoreV2 { /** Get statuses by [userWalletId] */ fun get(userWalletId: UserWalletId): Flow> + /** Get status of [network] by [userWalletId] synchronously or null */ + suspend fun getSyncOrNull(userWalletId: UserWalletId, network: Network): SimpleNetworkStatus? + /** Refresh status of [network] by [userWalletId] */ suspend fun refresh(userWalletId: UserWalletId, network: Network) @@ -28,6 +31,9 @@ internal interface NetworksStatusesStoreV2 { */ suspend fun storeError(userWalletId: UserWalletId, network: Network, value: NetworkStatus.Unreachable? = null) + /** Store unreachable [value] by [userWalletId] */ + suspend fun storeUnreachableStatus(userWalletId: UserWalletId, value: NetworkStatus) + /** Store error for [networks] by [userWalletId] */ suspend fun storeError(userWalletId: UserWalletId, networks: Set) } \ No newline at end of file From 9581b398ad88a5a6b6a61cb2fa9164d1344d5554 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 17:11:48 +0300 Subject: [PATCH 12/19] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index ef2ebcb18f..ef3a5a7696 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.24.0-1056" +tangemBlockchainSdk = "releases-5.24.0-1062" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.24.0-467" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 08da4aaba926eaaab2bbc25d8ea883fbfc0dcf8f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 14:12:13 +0000 Subject: [PATCH 13/19] 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 ef3a5a7696..9e58bcf67c 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.24.0-1062" +tangemBlockchainSdk = "develop-1052" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.24.0-467" +tangemCardSdk = "develop-468" #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 ^ From 42b90de54daab00b8c95a997ae7d21ed3cea38bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 18:48:43 +0500 Subject: [PATCH 14/19] Updated on 2026-08-14 --- .../send/v2/common/CommonSendRoute.kt | 2 +- .../send/v2/common/utils/SendRouteUtils.kt | 28 +++++++++ .../send/v2/send/DefaultSendComponent.kt | 59 ++++++++++--------- .../v2/sendnft/DefaultNFTSendComponent.kt | 48 ++++++++------- 4 files changed, 87 insertions(+), 50 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/SendRouteUtils.kt diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt index 7493c4d073..e0889d71a4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt @@ -14,7 +14,7 @@ internal sealed class CommonSendRoute : Route { @Serializable data object Confirm : CommonSendRoute() { - override val isEditMode: Boolean = false + override val isEditMode: Boolean = true } @Serializable diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/SendRouteUtils.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/SendRouteUtils.kt new file mode 100644 index 0000000000..eba835865e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/SendRouteUtils.kt @@ -0,0 +1,28 @@ +package com.tangem.features.send.v2.common.utils + +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.value.Value +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.common.CommonSendRoute + +/** + * Workaround to try fix duplicate route crash + */ +internal fun Router.safeNextClick( + currentRoute: CommonSendRoute, + nextRoute: CommonSendRoute, + childStack: Value>, + popBack: () -> Unit, +) { + if (currentRoute.isEditMode) { + popBack() + } else { + val isAlreadyInStack = childStack.value.items.any { it.configuration == nextRoute } + if (isAlreadyInStack) { + popTo(nextRoute) + } else { + push(nextRoute) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 895ac9ac94..3f2eba602f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -26,6 +26,7 @@ import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.common.ui.SendContent import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.common.utils.safeNextClick import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.confirm.SendConfirmComponent import com.tangem.features.send.v2.send.model.SendModel @@ -139,28 +140,31 @@ internal class DefaultSendComponent @AssistedInject constructor( CommonSendRoute.Confirm -> getConfirmComponent(factoryContext) } - private fun getDestinationComponent(factoryContext: AppComponentContext, route: CommonSendRoute) = - SendDestinationComponent( - appComponentContext = factoryContext, - params = SendDestinationComponentParams.DestinationParams( - state = model.uiState.value.destinationUM, - currentRoute = currentRoute.filterIsInstance(), - isBalanceHidingFlow = model.isBalanceHiddenFlow, - analyticsCategoryName = analyticCategoryName, - title = resourceReference(R.string.send_recipient_label), - userWalletId = params.userWalletId, - cryptoCurrency = params.currency, - callback = model, - onBackClick = ::onChildBack, - onNextClick = { - if (route.isEditMode) { - onChildBack() - } else { - innerRouter.push(CommonSendRoute.Amount(isEditMode = false)) - } - }, - ), - ) + private fun getDestinationComponent( + factoryContext: AppComponentContext, + route: CommonSendRoute, + ): SendDestinationComponent = SendDestinationComponent( + appComponentContext = factoryContext, + params = SendDestinationComponentParams.DestinationParams( + state = model.uiState.value.destinationUM, + currentRoute = currentRoute.filterIsInstance(), + isBalanceHidingFlow = model.isBalanceHiddenFlow, + analyticsCategoryName = analyticCategoryName, + title = resourceReference(R.string.send_recipient_label), + userWalletId = params.userWalletId, + cryptoCurrency = params.currency, + callback = model, + onBackClick = ::onChildBack, + onNextClick = { + innerRouter.safeNextClick( + currentRoute = route, + nextRoute = CommonSendRoute.Amount(isEditMode = false), + popBack = ::onChildBack, + childStack = childStack, + ) + }, + ), + ) private fun getAmountComponent( factoryContext: AppComponentContext, @@ -196,11 +200,12 @@ internal class DefaultSendComponent @AssistedInject constructor( } }, onNextClick = { - if (route.isEditMode) { - onChildBack() - } else { - innerRouter.push(CommonSendRoute.Confirm) - } + innerRouter.safeNextClick( + currentRoute = route, + nextRoute = CommonSendRoute.Confirm, + popBack = ::onChildBack, + childStack = childStack, + ) }, ), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt index 7fc4f402c2..49bcae92d6 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt @@ -24,6 +24,7 @@ import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.ui.SendContent import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.common.utils.safeNextClick import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent import com.tangem.features.send.v2.sendnft.model.NFTSendModel @@ -126,28 +127,31 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( else -> getStubComponent() } - private fun getDestinationComponent(factoryContext: AppComponentContext, route: CommonSendRoute) = - SendDestinationComponent( - appComponentContext = factoryContext, - params = SendDestinationComponentParams.DestinationParams( - state = model.uiState.value.destinationUM, - currentRoute = currentRouteFlow.filterIsInstance(), - isBalanceHidingFlow = model.isBalanceHiddenFlow, - title = resourceReference(R.string.nft_send), - analyticsCategoryName = analyticsCategoryName, - userWalletId = params.userWalletId, - cryptoCurrency = model.cryptoCurrency, - callback = model, - onBackClick = ::onChildBack, - onNextClick = { - if (route.isEditMode) { - onChildBack() - } else { - innerRouter.push(CommonSendRoute.Confirm) - } - }, - ), - ) + private fun getDestinationComponent( + factoryContext: AppComponentContext, + route: CommonSendRoute, + ): SendDestinationComponent = SendDestinationComponent( + appComponentContext = factoryContext, + params = SendDestinationComponentParams.DestinationParams( + state = model.uiState.value.destinationUM, + currentRoute = currentRouteFlow.filterIsInstance(), + isBalanceHidingFlow = model.isBalanceHiddenFlow, + title = resourceReference(R.string.nft_send), + analyticsCategoryName = analyticsCategoryName, + userWalletId = params.userWalletId, + cryptoCurrency = model.cryptoCurrency, + callback = model, + onBackClick = ::onChildBack, + onNextClick = { + innerRouter.safeNextClick( + currentRoute = route, + nextRoute = CommonSendRoute.Confirm, + popBack = ::onChildBack, + childStack = childStack, + ) + }, + ), + ) private fun getFeeComponent(factoryContext: AppComponentContext): ComposableContentComponent { val state = model.uiState.value From 88b622053d64a0fbcc6f08aa3f4b52e1673d9c09 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 18:49:05 +0500 Subject: [PATCH 15/19] Updated on 2026-08-14 --- .../features/send/v2/common/PredefinedValues.kt | 6 +++--- .../tangem/features/send/v2/send/model/SendModel.kt | 9 ++++----- .../v2/subcomponents/amount/SendAmountComponent.kt | 2 +- .../subcomponents/amount/model/SendAmountModel.kt | 13 +++++++++---- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt index adbfd84dfc..ef94394fcf 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/PredefinedValues.kt @@ -4,19 +4,19 @@ sealed class PredefinedValues { data object Empty : PredefinedValues() sealed class Content : PredefinedValues() { - abstract val amount: String abstract val address: String + abstract val amount: String? abstract val memo: String? data class Deeplink( - override val amount: String, + override val amount: String?, override val address: String, override val memo: String?, val transactionId: String, ) : Content() data class QrCode( - override val amount: String, + override val amount: String?, override val address: String, override val memo: String?, ) : Content() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 250e790576..ff273f2b85 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -121,8 +121,8 @@ internal class SendModel @Inject constructor( _uiState.update { it.copy(destinationUM = destinationUM) } } - override fun onAmountResult(amountUM: AmountState) { - resetPredefinedAmount() + override fun onAmountResult(amountUM: AmountState, isResetPredefined: Boolean) { + if (isResetPredefined) resetPredefinedAmount() _uiState.update { it.copy(amountUM = amountUM) } } @@ -131,7 +131,6 @@ internal class SendModel @Inject constructor( } override fun onResult(sendUM: SendUM) { - resetPredefinedAmount() _uiState.update { sendUM } } @@ -163,8 +162,8 @@ internal class SendModel @Inject constructor( // reset predefined amount val internalPredefinedValues = predefinedValues predefinedValues = when (internalPredefinedValues) { - is PredefinedValues.Content.Deeplink -> internalPredefinedValues.copy(amount = "") - is PredefinedValues.Content.QrCode -> internalPredefinedValues.copy(amount = "") + is PredefinedValues.Content.Deeplink -> internalPredefinedValues.copy(amount = null) + is PredefinedValues.Content.QrCode -> internalPredefinedValues.copy(amount = null) PredefinedValues.Empty -> internalPredefinedValues } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt index f79c417846..b2a1c6a264 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt @@ -38,6 +38,6 @@ internal class SendAmountComponent( } interface ModelCallback : SendNavigationModelCallback { - fun onAmountResult(amountUM: AmountState) + fun onAmountResult(amountUM: AmountState, isResetPredefined: Boolean) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index 11974ef160..691bd074ca 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -103,9 +103,9 @@ internal class SendAmountModel @Inject constructor( ) } } - val predefinedValues = params.predefinedValues as? PredefinedValues.Content - if (predefinedValues?.amount != null) { - onAmountValueChange(predefinedValues.amount) + val predefinedAmount = (params.predefinedValues as? PredefinedValues.Content)?.amount + if (predefinedAmount != null) { + onAmountValueChange(predefinedAmount) } } @@ -220,7 +220,12 @@ internal class SendAmountModel @Inject constructor( private fun saveResult() { val params = params as? SendAmountComponentParams.AmountParams ?: return - params.callback.onAmountResult(uiState.value) + val predefinedAmount = (params.predefinedValues as? PredefinedValues.Content.QrCode)?.amount + val enteredAmount = (uiState.value as? AmountState.Data)?.amountTextField?.value + params.callback.onAmountResult( + amountUM = uiState.value, + isResetPredefined = predefinedAmount != enteredAmount, + ) } private fun configAmountNavigation() { From 8b47cd5aba7f58e7512bc9fc3a544ba365c1b606 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 19:25:43 +0500 Subject: [PATCH 16/19] Updated on 2026-08-14 --- .../tangem/core/ui/extensions/ModifierExt.kt | 30 +++++++++++++++++++ .../core/ui/utils/MultipleClickPreventer.kt | 10 ++++++- .../v2/common/ui/SendNavigationButtons.kt | 19 ++++++++---- 3 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt new file mode 100644 index 0000000000..469e048635 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt @@ -0,0 +1,30 @@ +package com.tangem.core.ui.extensions + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.semantics.Role +import com.tangem.core.ui.utils.MultipleClickPreventer + +/** + * Clickable modifier with debounce multiple click in a short period of time + */ +fun Modifier.clickableSingle( + enabled: Boolean = true, + onClickLabel: String? = null, + role: Role? = null, + onClick: () -> Unit, +) = composed { + val multipleEventsCutter = remember { MultipleClickPreventer.get() } + Modifier.clickable( + enabled = enabled, + onClickLabel = onClickLabel, + onClick = { multipleEventsCutter.processEvent { onClick() } }, + role = role, + indication = LocalIndication.current, + interactionSource = remember { MutableInteractionSource() }, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/MultipleClickPreventer.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/MultipleClickPreventer.kt index 15ede204ae..767e1ada4d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/MultipleClickPreventer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/MultipleClickPreventer.kt @@ -2,6 +2,9 @@ package com.tangem.core.ui.utils import android.os.SystemClock +/** + * Debounce multiple events (e.i. clicks, etc) in a short period of time + */ interface MultipleClickPreventer { fun processEvent(event: () -> Unit) @@ -24,4 +27,9 @@ private class DefaultMultipleClickPreventer : MultipleClickPreventer { companion object { private const val TIME_BETWEEN_CLICK_IN_MS = 500L } -} \ No newline at end of file +} + +/** + * Debounce multiple events (e.i. clicks, etc) in a short period of time + */ +fun singleEvent(event: () -> Unit) = MultipleClickPreventer.get().processEvent(event) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt index 581e14ba92..70d852dff8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt @@ -2,7 +2,6 @@ package com.tangem.features.send.v2.common.ui import androidx.compose.animation.* import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -26,10 +25,12 @@ import com.tangem.core.ui.components.SpacerW12 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.features.send.v2.send.ui.state.ButtonsUM +import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.singleEvent import com.tangem.features.send.v2.common.ui.state.NavigationUM +import com.tangem.features.send.v2.send.ui.state.ButtonsUM @Composable internal fun SendNavigationButtons(navigationUM: NavigationUM, modifier: Modifier = Modifier) { @@ -70,7 +71,7 @@ private fun SendNavigationButton(navigationUM: NavigationUM, modifier: Modifier modifier = Modifier .clip(RoundedCornerShape(16.dp)) .background(TangemTheme.colors.button.secondary) - .clickable(onClick = wrappedNavigationUM.onClick) + .clickableSingle(onClick = wrappedNavigationUM.onClick) .padding(12.dp), ) SpacerW12() @@ -110,7 +111,11 @@ private fun SendDoneButtons(pairButtonsUM: ButtonsUM.SecondaryPairButtonsUM?, mo SecondaryButtonIconStart( text = wrappedPairButtonsUM.leftText.resolveReference(), iconResId = wrappedPairButtonsUM.leftIconResId!!, - onClick = wrappedPairButtonsUM.onLeftClick, + onClick = { + singleEvent { + wrappedPairButtonsUM.onLeftClick + } + }, modifier = Modifier.weight(1f), ) SpacerW12() @@ -118,8 +123,10 @@ private fun SendDoneButtons(pairButtonsUM: ButtonsUM.SecondaryPairButtonsUM?, mo text = wrappedPairButtonsUM.rightText.resolveReference(), iconResId = wrappedPairButtonsUM.rightIconResId!!, onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - wrappedPairButtonsUM.onRightClick() + singleEvent { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + wrappedPairButtonsUM.onRightClick() + } }, modifier = Modifier.weight(1f), ) From 597ff6b82e58033c9cb2e11cf07c9cb4973e3e03 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 15:26:21 +0000 Subject: [PATCH 17/19] 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 ef3a5a7696..ccc406b5ff 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.24.0-1062" +tangemBlockchainSdk = "develop-1063" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.24.0-467" +tangemCardSdk = "develop-468" #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 ^ From 2aaaf19ea88abb52f1841512e50309accffd7019 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 18:47:23 +0300 Subject: [PATCH 18/19] Updated on 2026-08-14 --- .../networks/multi/DefaultMultiNetworkStatusFetcher.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt index cd67ca7880..eea619bd3b 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -5,9 +5,9 @@ import arrow.core.raise.ensure import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.networks.store.NetworksStatusesStoreV2 -import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -15,7 +15,6 @@ import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.utils.eitherOn -import com.tangem.domain.demo.DemoConfig import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.tokens.model.CryptoCurrency @@ -42,10 +41,9 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( private val appPreferencesStore: AppPreferencesStore, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, private val dispatchers: CoroutineDispatcherProvider, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ) : MultiNetworkStatusFetcher { - private val demoConfig = DemoConfig() - private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains) private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory(excludedBlockchains) } override suspend fun invoke(params: MultiNetworkStatusFetcher.Params) = eitherOn(dispatchers.default) { @@ -106,7 +104,7 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() if (!blockchains.contains(cardBlockchain)) return emptySet() - return cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse).toSet() + return cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse).toSet() } private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, networks: Set): Set { From d46b4c19a739c636d8cdec6be6bcae4a9a6b06bf Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 18:50:10 +0300 Subject: [PATCH 19/19] Updated on 2026-08-14 --- .../data/networks/multi/DefaultMultiNetworkStatusFetcher.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt index eea619bd3b..12b5b8fd81 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -34,6 +34,7 @@ import javax.inject.Inject * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") internal class DefaultMultiNetworkStatusFetcher @Inject constructor( excludedBlockchains: ExcludedBlockchains, private val networksStatusesStore: NetworksStatusesStoreV2,