From 9736fb5ce8c06d138059c8a22558d45645dd3133 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Mar 2026 16:30:22 +0300 Subject: [PATCH 01/25] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index b693d601d9..827750b5a0 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit b693d601d9dc3de27f0536574bce59a6aa5e2e89 +Subproject commit 827750b5a0ef7202120b0df0ae903d70e18e3d32 From 3935ed76357a04382ab604c1436f4ce6ccc4afdf Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Mar 2026 16:59:07 +0100 Subject: [PATCH 02/25] Updated on 2026-08-14 --- .../domain/tangempay/TangemPayAnalyticsEvents.kt | 10 ++++++++++ .../com/tangem/features/details/model/DetailsModel.kt | 2 ++ .../wallet/model/intents/TangemPayClickIntents.kt | 1 + 3 files changed, 13 insertions(+) diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt index b3de223ee0..f7eb452cc2 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt @@ -191,4 +191,14 @@ sealed class TangemPayAnalyticsEvents( categoryName = "Visa Onboarding", event = "Visa KYC Canceled", ) + + class MainVisaPermanentBannerClicked : TangemPayAnalyticsEvents( + categoryName = "Visa Onboarding", + event = "Visa Permanent Banner Clicked", + ) + + class DetailsVisaPermanentButtonClicked : TangemPayAnalyticsEvents( + categoryName = "Visa Onboarding", + event = "Visa Permanent Button Clicked", + ) } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index 8ebb80ce0b..1cf9bc5fc0 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -23,6 +23,7 @@ import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase +import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -264,6 +265,7 @@ internal class DetailsModel @Inject constructor( private fun onTangemPayItemClicked() { modelScope.launch { + analyticsEventHandler.send(TangemPayAnalyticsEvents.DetailsVisaPermanentButtonClicked()) val isEligible = tangemPayEligibilityManager.getTangemPayAvailability() if (isEligible) { router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings)) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index 0c3e58467e..debd9cec80 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -245,6 +245,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( override fun onOnboardingBannerClick(userWalletId: UserWalletId) { modelScope.launch { + analyticsEventHandler.send(TangemPayAnalyticsEvents.MainVisaPermanentBannerClicked()) val isEligible = tangemPayEligibilityManager.getTangemPayAvailability() if (isEligible) { router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain) From 33896ea98a5e82a59c9c5eef4a919995a0962741 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Mar 2026 19:08:41 +0300 Subject: [PATCH 03/25] 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 a3bc5cab8e..9a63fac2c9 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-587" +tangemCardSdk = "releases-5.34-591" #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 f77389cd31aa90b0a980498b72819fadd3e1ce2c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Mar 2026 21:14:22 +0400 Subject: [PATCH 04/25] Updated on 2026-08-14 --- .../com/tangem/tap/data/DefaultCardSdkProvider.kt | 2 +- .../common/repository/DefaultMnemonicRepository.kt | 11 ++++------- gradle/tangem_dependencies.toml | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) 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 784db9dbc2..9918edc9f1 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt @@ -168,7 +168,7 @@ internal class DefaultCardSdkProvider @Inject constructor( secureStorage = secureStorage, authenticationManager = authenticationManager, keystoreManager = keystoreManager, - wordlist = Wordlist.getWordlist(activity), + wordlist = Wordlist.getWordlist(), config = config.apply { val apiConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemTech) tangemApiBaseUrl = apiConfig.baseUrl diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/repository/DefaultMnemonicRepository.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/repository/DefaultMnemonicRepository.kt index 0c3b768d31..921f2d9db3 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/repository/DefaultMnemonicRepository.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/repository/DefaultMnemonicRepository.kt @@ -1,6 +1,5 @@ package com.tangem.features.hotwallet.common.repository -import android.content.Context import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.bip39.EntropyLength import com.tangem.crypto.bip39.Mnemonic @@ -8,15 +7,13 @@ import com.tangem.crypto.bip39.Wordlist import com.tangem.features.hotwallet.MnemonicRepository import com.tangem.features.hotwallet.MnemonicRepository.MnemonicType import com.tangem.sdk.extensions.getWordlist -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -internal class DefaultMnemonicRepository @Inject constructor( - @ApplicationContext private val context: Context, -) : MnemonicRepository { - private val wordlist = Wordlist.getWordlist(context) +internal class DefaultMnemonicRepository @Inject constructor() : MnemonicRepository { - override val words: Set = wordlist.words.toHashSet() + private val wordlist by lazy(LazyThreadSafetyMode.NONE) { Wordlist.getWordlist() } + + override val words: Set by lazy(LazyThreadSafetyMode.NONE) { wordlist.words.toHashSet() } override fun generateMnemonic(type: MnemonicType): Mnemonic = DefaultMnemonic( entropy = when (type) { diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 9a63fac2c9..bcb442df0a 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ tangemCardSdk = "releases-5.34-591" #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 ^ -tangemHotSdk = "develop-539" +tangemHotSdk = "develop-547" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 863d12763d923f91a604b4af066bf19ac915b3de Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Mar 2026 12:19:46 +0300 Subject: [PATCH 05/25] Updated on 2026-08-14 --- .../datasource/asset/loader/AssetLoader.kt | 64 +++++++++++++++---- .../asset/reader/AndroidAssetReader.kt | 6 +- .../asset/loader/AssetLoaderTest.kt | 3 + .../asset/reader/AndroidAssetReaderTest.kt | 4 +- 4 files changed, 58 insertions(+), 19 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt index 2bab1747cf..9325df9a70 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt @@ -3,10 +3,13 @@ package com.tangem.datasource.asset.loader import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.squareup.moshi.adapter -import com.tangem.utils.coroutines.runCatching +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.datasource.asset.reader.AssetReader import com.tangem.datasource.di.NetworkMoshi import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -25,25 +28,44 @@ import javax.inject.Singleton class AssetLoader @Inject constructor( val assetReader: AssetReader, @NetworkMoshi val moshi: Moshi, + val analyticsExceptionHandler: AnalyticsExceptionHandler, val dispatchers: CoroutineDispatcherProvider, ) { /** Load content [Content] of asset file [fileName] */ + @Suppress("SuspendFunSwallowedCancellation") @OptIn(ExperimentalStdlibApi::class) - suspend inline fun load(fileName: String): Content? = runCatching(dispatchers.io) { - val json = assetReader.read(fullFileName = "$fileName.json") + suspend inline fun load(fileName: String): Content? = withContext(dispatchers.io) { + val json = runCatching { assetReader.read(fullFileName = "$fileName.json") }.getOrNull() - moshi.adapter().fromJson(json) + runCatching { + moshi.adapter().fromJson(json) + } + .fold( + onSuccess = { parsedConfig -> + if (parsedConfig == null) { + sendException( + fileName = fileName, + isParsingSuccess = true, + json = json, + ) + + Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + } + + parsedConfig + }, + onFailure = { throwable -> + sendException( + fileName = fileName, + isParsingSuccess = false, + json = json, + ) + + Timber.e(throwable, "Failed to load config [$fileName] from assets") + null + }, + ) } - .fold( - onSuccess = { parsedConfig -> - if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) - parsedConfig - }, - onFailure = { throwable -> - Timber.e(throwable, "Failed to load config [$fileName] from assets") - null - }, - ) /** Load list [V] values of asset file [fileName] */ suspend inline fun loadList(fileName: String): List = runCatching(dispatchers.io) { @@ -84,4 +106,18 @@ class AssetLoader @Inject constructor( emptyMap() }, ) + + fun sendException(fileName: String, isParsingSuccess: Boolean, json: String?) { + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = IllegalStateException("Parsing config is failed"), + params = mapOf( + "filename" to fileName, + "isParsingSuccess" to isParsingSuccess.toString(), + "json_size" to json?.length.toString(), + "json" to json?.take(n = 30).toString(), + ), + ), + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt index 3847d29980..b2c9c5b2e2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt @@ -1,7 +1,6 @@ package com.tangem.datasource.asset.reader import android.content.res.AssetManager -import java.io.BufferedReader /** * Implementation of asset file reader @@ -13,7 +12,8 @@ internal class AndroidAssetReader( ) : AssetReader { override suspend fun read(fullFileName: String): String { - return assetManager.open(fullFileName).bufferedReader() - .use(BufferedReader::readText) + return assetManager.open(fullFileName, AssetManager.ACCESS_BUFFER).use { inputStream -> + inputStream.readBytes().toString(Charsets.UTF_8) + } } } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt index 1bbee64573..a1594149d3 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt @@ -5,6 +5,7 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.squareup.moshi.adapter +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.datasource.api.express.models.response.Asset import com.tangem.datasource.asset.reader.AssetReader import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider @@ -23,9 +24,11 @@ class AssetLoaderTest { private val assetReader = mockk() private val moshi = mockk() + private val analyticsExceptionHandler = mockk() private val assetLoader = AssetLoader( assetReader = assetReader, moshi = moshi, + analyticsExceptionHandler = analyticsExceptionHandler, dispatchers = TestingCoroutineDispatcherProvider(), ) diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt index 651d234c8d..93425e2814 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt @@ -18,7 +18,7 @@ internal class AndroidAssetReaderTest { @Test fun read_content() = runTest { - every { assetManager.open(FILE_NAME) } returns json.byteInputStream() + every { assetManager.open(FILE_NAME, AssetManager.ACCESS_BUFFER) } returns json.byteInputStream() val actual = assetReader.read(fullFileName = FILE_NAME) @@ -28,7 +28,7 @@ internal class AndroidAssetReaderTest { @Test fun read_error() = runTest { val exception = IOException("Error") - every { assetManager.open(FILE_NAME) } throws exception + every { assetManager.open(FILE_NAME, AssetManager.ACCESS_BUFFER) } throws exception runCatching { assetReader.read(fullFileName = FILE_NAME) } .onSuccess { throw IllegalStateException("Error should be thrown") } From ee556fefaf99a955d55b9d78897da3d737b9d693 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Mar 2026 20:25:30 +0400 Subject: [PATCH 06/25] Updated on 2026-08-14 --- .../HasSingleWalletSignedHashesUseCase.kt | 32 +++++++++++-------- .../transformers/DeleteWalletTransformer.kt | 5 +++ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt index 088110a3cb..cb7eb1f324 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt @@ -1,14 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.network.Network -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.walletmanager.WalletManagersFacade import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import timber.log.Timber import javax.inject.Inject @ModelScoped @@ -27,18 +28,23 @@ class HasSingleWalletSignedHashesUseCase @Inject constructor( return@map false } - return@map walletManagersFacade.validateSignatureCount( - userWalletId = userWallet.walletId, - network = network, - signedHashes = userWallet.scanResponse.card.wallets.firstOrNull()?.totalSignedHashes ?: 0, - ) - .fold( - ifLeft = { true }, - ifRight = { - cardRepository.setCardWasScanned(cardId = userWallet.cardId) - false - }, + return@map try { + walletManagersFacade.validateSignatureCount( + userWalletId = userWallet.walletId, + network = network, + signedHashes = userWallet.scanResponse.card.wallets.firstOrNull()?.totalSignedHashes ?: 0, ) + .fold( + ifLeft = { true }, + ifRight = { + cardRepository.setCardWasScanned(cardId = userWallet.cardId) + false + }, + ) + } catch (e: IllegalArgumentException) { + Timber.w(e, "Unable to validate signature count: user wallet not found") + false + } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt index bf2960f3d7..f30c88927b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt @@ -17,6 +17,11 @@ internal class DeleteWalletTransformer( val deletedWalletUM = prevState.getDeletedWalletState2() return when { + deletedWalletUM != null && deletedWalletState != null -> prevState.copy( + selectedWalletIndex = selectedWalletIndex, + wallets = (prevState.wallets - deletedWalletState).toImmutableList(), + wallets2 = (prevState.wallets2 - deletedWalletUM).toImmutableList(), + ) deletedWalletUM != null -> prevState.copy( selectedWalletIndex = selectedWalletIndex, wallets2 = (prevState.wallets2 - deletedWalletUM).toImmutableList(), From 0e86fd855360259a1952ffbe2171733959b4393f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Mar 2026 14:58:58 +0400 Subject: [PATCH 07/25] Updated on 2026-08-14 --- .../java/com/tangem/tap/ApplicationEntryPoint.kt | 3 +++ .../main/java/com/tangem/tap/TangemApplication.kt | 9 ++++++++- .../common/analytics/CustomerIoFeatureToggles.kt | 12 ++++++++++++ .../pushes/TangemPushNotificationService.kt | 15 +++++++++++++-- .../assets/configs/feature_toggles_config.json | 4 ++++ 5 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/CustomerIoFeatureToggles.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index ba01875acf..54e4f52423 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -43,6 +43,7 @@ import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.tap.common.analytics.CustomerIoFeatureToggles import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient import com.tangem.tap.common.log.TangemAppLoggerInitializer @@ -151,4 +152,6 @@ interface ApplicationEntryPoint { fun getABTestsManager(): ABTestsManager fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory + + fun getCustomerIoFeatureToggles(): CustomerIoFeatureToggles } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 8cf20b1b8a..3fe333e207 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -61,6 +61,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.attestation.api.TangemApiServiceSettings import com.tangem.tap.common.analytics.AnalyticsFactory +import com.tangem.tap.common.analytics.CustomerIoFeatureToggles import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler @@ -239,6 +240,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val appsFlyerClientFactory: AppsFlyerClient.Factory get() = entryPoint.getAppsFlyerClientFactory() + private val customerIoFeatureToggles: CustomerIoFeatureToggles + get() = entryPoint.getCustomerIoFeatureToggles() + // endregion private val appScope = MainScope() @@ -411,7 +415,10 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder()) factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder()) factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder(appsFlyerClientFactory)) - factory.addHandlerBuilder(CustomerIoAnalyticsHandler.Builder()) + + if (customerIoFeatureToggles.isFeatureEnabled) { + factory.addHandlerBuilder(CustomerIoAnalyticsHandler.Builder()) + } factory.addFilter(oneTimeEventFilter) factory.addFilter(AppsFlyerEventFilter()) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/CustomerIoFeatureToggles.kt b/app/src/main/java/com/tangem/tap/common/analytics/CustomerIoFeatureToggles.kt new file mode 100644 index 0000000000..ae70817e3a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/CustomerIoFeatureToggles.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.common.analytics + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import javax.inject.Inject + +class CustomerIoFeatureToggles @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) { + + val isFeatureEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "CUSTOMER_IO_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt index 7c584a3bd6..714a4acfd7 100644 --- a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -3,12 +3,19 @@ package com.tangem.tap.common.pushes import android.annotation.SuppressLint import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +import com.tangem.tap.common.analytics.CustomerIoFeatureToggles +import dagger.hilt.android.AndroidEntryPoint import io.customer.messagingpush.CustomerIOFirebaseMessagingService import timber.log.Timber +import javax.inject.Inject +@AndroidEntryPoint @SuppressLint("MissingFirebaseInstanceTokenRefresh") internal class TangemPushNotificationService : FirebaseMessagingService() { + @Inject + lateinit var customerIoFeatureToggles: CustomerIoFeatureToggles + private val pushNotificationDelegate: PushNotificationDelegate by lazy { PushNotificationDelegate(applicationContext) } @@ -17,13 +24,17 @@ internal class TangemPushNotificationService : FirebaseMessagingService() { super.onNewToken(token) Timber.d("New FCM token received: $token") - CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token) + if (customerIoFeatureToggles.isFeatureEnabled) { + CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token) + } } override fun onMessageReceived(message: RemoteMessage) { super.onMessageReceived(message) - CustomerIOFirebaseMessagingService.onMessageReceived(applicationContext, message) + if (customerIoFeatureToggles.isFeatureEnabled) { + CustomerIOFirebaseMessagingService.onMessageReceived(applicationContext, message) + } val notification = message.notification ?: return val channelId = notification.channelId ?: TANGEM_CHANNEL_ID diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 73b02b2af8..90eb39ed7b 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -55,5 +55,9 @@ { "name": "MULTI_ADDRESS_UTXO_ENABLED", "version": "undefined" + }, + { + "name": "CUSTOMER_IO_ENABLED", + "version": "undefined" } ] From 3b51b657fe17386a03bb898ee300b07dcb175706 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Mar 2026 14:56:41 +0400 Subject: [PATCH 08/25] Updated on 2026-08-14 --- .../tokendetails/model/TokenDetailsModel.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 2f8b9f76ed..0dc6645e21 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -738,15 +738,20 @@ internal class TokenDetailsModel @Inject constructor( analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrency.symbol)) modelScope.launch { - val canHide = cryptoCurrency is CryptoCurrency.Coin && isCryptoCurrencyCoinCouldHideUseCase( - userWalletId = userWalletId, - cryptoCurrencyCoin = cryptoCurrency, - ) + val canHide = when (cryptoCurrency) { + is CryptoCurrency.Coin -> { + isCryptoCurrencyCoinCouldHideUseCase( + userWalletId = userWalletId, + cryptoCurrencyCoin = cryptoCurrency, + ) + } + is CryptoCurrency.Token -> true + } - internalUiState.value = if (!canHide) { - stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency) - } else { + internalUiState.value = if (canHide) { stateFactory.getStateWithConfirmHideTokenDialog(cryptoCurrency) + } else { + stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency) } } } From 09c679b91d7159677170eed23e40a090fa6732c7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Mar 2026 16:29:45 +0300 Subject: [PATCH 09/25] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 73b02b2af8..00bbf7c0c4 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -34,11 +34,11 @@ }, { "name": "EARN_BLOCK_ENABLED", - "version": "undefined" + "version": "5.35" }, { "name": "HOLD_TO_CONFIRM_BUTTON_ENABLED", - "version": "undefined" + "version": "5.35" }, { "name": "WALLET_REORDER_FEATURE_ENABLED", From 8fa0d16c166b68e70e989742f041c8b218efe112 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Mar 2026 16:34:36 +0300 Subject: [PATCH 10/25] Updated on 2026-08-14 --- .../wallet/model/intents/WalletCurrencyActionsClickIntents.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index b57effd249..3635996d11 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.child.wallet.model.intents +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.blockchain.common.address.AddressType import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter @@ -288,7 +289,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) }, ifRight = { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = accountId.userWalletId)) + router.dialogNavigation.dismiss() }, ) } From d10df0569a92c157e9a408d58703dd7ca22d350a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Mar 2026 18:34:46 +0500 Subject: [PATCH 11/25] Updated on 2026-08-14 --- .../model/converter/TokenListToStateConverter.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt index d6fbf57989..2d1b76c423 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt @@ -5,7 +5,6 @@ import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem @@ -70,7 +69,7 @@ internal class AccountTokenItemConverter( tokenItemState = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = accountStatus.account, - ).convert(TotalFiatBalance.Loading), + ).convert(accountStatus.tokenList.totalFiatBalance), ), ) if (isGrouping) { From f063bd6bb8edca2802a24d308f6ca8d0479976d1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Mar 2026 18:35:21 +0500 Subject: [PATCH 12/25] Updated on 2026-08-14 --- .../impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt index d4c055fcda..990778ca91 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt @@ -59,7 +59,7 @@ internal fun MultiWalletAccessCodeEnter( val focusRequester = remember { FocusRequester() } OutlineTextField( - modifier = modifier + modifier = Modifier .focusRequester(focusRequester) .fillMaxWidth(), value = if (reEnterAccessCodeState) { @@ -73,12 +73,14 @@ internal fun MultiWalletAccessCodeEnter( state.onAccessCodeFirstChange }, label = stringResourceSafe(id = R.string.onboarding_wallet_info_title_third), - isError = state.codesNotMatchError, + isError = state.codesNotMatchError || state.atLeast4CharError, visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), caption = when { state.codesNotMatchError && reEnterAccessCodeState -> stringResourceSafe(R.string.onboarding_access_codes_doesnt_match) + state.atLeast4CharError && !reEnterAccessCodeState -> + stringResourceSafe(R.string.onboarding_access_code_too_short) else -> null }, ) From e3cd02a5484adc6e10f8ebb5ef6c33ff2f10dab7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Mar 2026 16:42:59 +0300 Subject: [PATCH 13/25] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 827750b5a0..c99f133eab 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 827750b5a0ef7202120b0df0ae903d70e18e3d32 +Subproject commit c99f133eabd0127a1bac3cb1a631c6c8d4fa9d66 From 0c3d2c0cd75b2a83568442692a11fa78e77e6994 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Mar 2026 17:53:43 +0300 Subject: [PATCH 14/25] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../converter/GeneratedEnvironmentConfigConverter.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index c99f133eab..0f6e9e0417 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit c99f133eabd0127a1bac3cb1a631c6c8d4fa9d66 +Subproject commit 0f6e9e0417e302fa8dd282f75f212d0421890563 diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt index fb57e24aee..ae13b4cae4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt @@ -51,7 +51,7 @@ internal object GeneratedEnvironmentConfigConverter { bffStaticTokenDev = GeneratedEnvironmentConfig.bffStaticTokenDev, gaslessTxApiKeyDev = GeneratedEnvironmentConfig.gaslessTxApiKeyDev, gaslessTxApiKey = GeneratedEnvironmentConfig.gaslessTxApiKey, - customerIoCdpApiKey = GeneratedEnvironmentConfig.CustomerIO.appApiKey, + customerIoCdpApiKey = GeneratedEnvironmentConfig.CustomerIO.andriodApiKey, surveySparrowToken = GeneratedEnvironmentConfig.SurveySparrow.apiKey, ) } From f9eaa17630e277740f04ebe598c774e0ec35d3d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Mar 2026 18:00:43 +0300 Subject: [PATCH 15/25] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 0f6e9e0417..636d7a8aa0 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 0f6e9e0417e302fa8dd282f75f212d0421890563 +Subproject commit 636d7a8aa0e330e9b95e91d85f23ad15ac6d913f From 00c8ef60e902bd87c05146ee6be412c8194d5967 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Mar 2026 18:16:20 +0300 Subject: [PATCH 16/25] Updated on 2026-08-14 --- .../converter/GeneratedEnvironmentConfigConverter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt index ae13b4cae4..28644f5c6b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt @@ -51,7 +51,7 @@ internal object GeneratedEnvironmentConfigConverter { bffStaticTokenDev = GeneratedEnvironmentConfig.bffStaticTokenDev, gaslessTxApiKeyDev = GeneratedEnvironmentConfig.gaslessTxApiKeyDev, gaslessTxApiKey = GeneratedEnvironmentConfig.gaslessTxApiKey, - customerIoCdpApiKey = GeneratedEnvironmentConfig.CustomerIO.andriodApiKey, + customerIoCdpApiKey = GeneratedEnvironmentConfig.CustomerIO.androidApiKey, surveySparrowToken = GeneratedEnvironmentConfig.SurveySparrow.apiKey, ) } From 431a969a43c032840d243ab577c4958db0970fa9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Mar 2026 20:12:38 +0400 Subject: [PATCH 17/25] Updated on 2026-08-14 --- .../common/analytics/handlers/customerio/CustomerIoClient.kt | 2 ++ .../src/main/assets/configs/feature_toggles_config.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt index 834244e2ec..ddd534e938 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt @@ -4,6 +4,7 @@ import android.app.Application import io.customer.messagingpush.ModuleMessagingPushFCM import io.customer.sdk.CustomerIO import io.customer.sdk.CustomerIOBuilder +import io.customer.sdk.data.model.Region import timber.log.Timber /** @@ -25,6 +26,7 @@ internal class CustomerIoClient( applicationContext = application, cdpApiKey = cdpApiKey, ) + .region(Region.EU) .trackApplicationLifecycleEvents(false) .autoTrackActivityScreens(false) .addCustomerIOModule(ModuleMessagingPushFCM()) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 0b7760b0cf..7399fee331 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -58,6 +58,6 @@ }, { "name": "CUSTOMER_IO_ENABLED", - "version": "undefined" + "version": "5.35" } ] From 6c7c9bc3cb240039d6a0ae59dfde21817c2bebb5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Mar 2026 13:00:29 +0300 Subject: [PATCH 18/25] Updated on 2026-08-14 --- .../common/TangemSiteShareUrlBuilder.kt | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt index 64554a30e0..1830c50c92 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt @@ -1,31 +1,11 @@ package com.tangem.common -import com.tangem.utils.SupportedLanguages.CHINESE -import com.tangem.utils.SupportedLanguages.ENGLISH -import com.tangem.utils.SupportedLanguages.FRANCH -import com.tangem.utils.SupportedLanguages.GERMAN -import com.tangem.utils.SupportedLanguages.JAPANESE -import java.util.Locale - object TangemSiteShareUrlBuilder { private const val BASE_URL = "https://tangem.com" private const val CRYPTOCURRENCIES_PATH = "cryptocurrencies" - @Deprecated("Should use CHINESE from SupportedLanguages, but the site expects zh-Hans in the URL path") - private const val CHINESE_SITE_LOCALE = "zh-Hans" - - @Deprecated("Should rely on SupportedLanguages instead of maintaining a separate list") - private val siteLocales = mapOf( - ENGLISH to ENGLISH, - FRANCH to FRANCH, - GERMAN to GERMAN, - JAPANESE to JAPANESE, - CHINESE to CHINESE_SITE_LOCALE, - ) - fun shareUrl(tokenId: String): String { - val locale = siteLocales[Locale.getDefault().language] ?: ENGLISH - return "$BASE_URL/$locale/$CRYPTOCURRENCIES_PATH/$tokenId" + return "$BASE_URL/$CRYPTOCURRENCIES_PATH/$tokenId" } } \ No newline at end of file From 37023a15b42863f791dfc27b8ecb964119d416c5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Mar 2026 15:33:44 +0400 Subject: [PATCH 19/25] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 118 +++++++------- .../DefaultTangemPayWithdrawRepository.kt | 149 +++++++++++------- .../tangempay/model/TangemPayDetailsModel.kt | 9 -- .../subscribers/TangemPayMainSubscriber.kt | 6 + 4 files changed, 153 insertions(+), 129 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index 0d9a4eba92..595d7ccdcb 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -62,23 +62,19 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) { - withContext(dispatcherProvider.io) { - appPreferencesStore - .store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress) - } + appPreferencesStore + .store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress) } override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId)) - .takeIf { !it.isNullOrEmpty() } - } + return appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), + ) + .takeIf { !it.isNullOrEmpty() } } override suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") - } + appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") } override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) = @@ -92,58 +88,57 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? { - val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress)) - ?.decodeToString(throwOnInvalidSequence = true) - ?.let(tokensAdapter::fromJson) + return withContext(dispatcherProvider.io) { + val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress)) + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(tokensAdapter::fromJson) - return authTokens?.let { tokens -> - if (tokens.idempotencyKey == null) { - val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString()) - storeAuthTokens(customerWalletAddress, newAuthTokens) - newAuthTokens - } else { - tokens + authTokens?.let { tokens -> + if (tokens.idempotencyKey == null) { + val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString()) + storeAuthTokens(customerWalletAddress, newAuthTokens) + newAuthTokens + } else { + tokens + } } } } override suspend fun clearAuthTokens(customerWalletAddress: String) { - secureStorage.delete(createAuthTokensKey(customerWalletAddress)) + withContext(dispatcherProvider.io) { + secureStorage.delete(createAuthTokensKey(customerWalletAddress)) + } } override suspend fun storeOrderId(customerWalletAddress: String, orderId: String) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId) - } + appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId) } override suspend fun getOrderId(customerWalletAddress: String): String? { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress)) - .takeIf { !it.isNullOrEmpty() } - } + return appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress)) + .takeIf { !it.isNullOrEmpty() } } override suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrNull( - key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), - ) == true - } + return appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), + ) == true } override suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone) - } + appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone) } - override suspend fun clearOrderId(customerWalletAddress: String) = withContext(dispatcherProvider.io) { + override suspend fun clearOrderId(customerWalletAddress: String) { appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") } override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) { - appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), isPaeraCustomer) + appPreferencesStore.store( + PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), + isPaeraCustomer, + ) } override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? { @@ -152,7 +147,9 @@ internal class DefaultTangemPayStorage @Inject constructor( override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + val orders = mutablePreferences.getObjectMap( + PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, + ) .plus(createWithdrawOrderIdKey(userWalletId) to orderId) mutablePreferences.setObjectMap( key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, @@ -179,7 +176,9 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? { - val orders = appPreferencesStore.getObjectMapSync(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + val orders = appPreferencesStore.getObjectMapSync( + PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, + ) return orders[createWithdrawOrderIdKey(userWalletId)] } @@ -191,7 +190,9 @@ internal class DefaultTangemPayStorage @Inject constructor( override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + val orders = mutablePreferences.getObjectMap( + PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, + ) .minus(createWithdrawOrderIdKey(userWalletId)) mutablePreferences.setObjectMap( key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, @@ -221,40 +222,33 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide) - } + appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide) } override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrNull( - key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), - ) == true - } + return appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), + ) == true } override suspend fun storeTangemPayEligibility(eligibility: Boolean) { - withContext(dispatcherProvider.io) { - appPreferencesStore.store(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, value = eligibility) - } + appPreferencesStore.store(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, value = eligibility) } override suspend fun getTangemPayEligibility(): Boolean { - return withContext(dispatcherProvider.io) { - appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, default = false) - } + return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY, default = false) } - override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) = + override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) { withContext(dispatcherProvider.io) { secureStorage.delete(createAuthTokensKey(customerWalletAddress)) - appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false) - appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") - appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") - appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false) - appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false) } + appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false) + appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") + appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") + appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false) + appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false) + } private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address" diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt index 7bf21432a7..08686ded72 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt @@ -23,7 +23,13 @@ import com.tangem.domain.visa.error.VisaApiError import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.utils.extensions.addHexPrefix -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber @@ -36,6 +42,8 @@ import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration.Companion.seconds private const val TAG = "TangemPaySwapRepository" +private const val MAX_POLLING_ATTEMPTS = 6 +private data class PollingKey(val userWalletId: String, val orderId: String) @Suppress("LongParameterList") internal class DefaultTangemPayWithdrawRepository @Inject constructor( @@ -48,9 +56,9 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( private val orderRepository: CustomerOrderRepository, ) : TangemPayWithdrawRepository { - private val withdrawPollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - private val withdrawPollingJobs = mutableMapOf() - private val withdrawPollingMutex = Mutex() + private val pollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val pollingJobs = mutableMapOf() + private val pollingMutex = Mutex() override suspend fun withdraw( userWallet: UserWallet, @@ -121,6 +129,76 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( ) } else { tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) + startPollingForOrder(userWallet, orderId, exchangeData) + } + } + } + + private fun startPollingForOrder( + userWallet: UserWallet, + orderId: String, + exchangeData: TangemPayWithdrawExchangeState, + ) { + pollingScope.launch { + startWithdrawOrderPolling(userWallet, orderId, exchangeData) + } + } + + private suspend fun startWithdrawOrderPolling( + userWallet: UserWallet, + orderId: String, + exchangeData: TangemPayWithdrawExchangeState, + ) { + val key = PollingKey(userWallet.walletId.stringValue, orderId) + + pollingMutex.withLock { + if (pollingJobs.containsKey(key)) return@withLock + + val pollingJob = pollingScope.launch { + try { + var attemptCount = 0 + while (isActive && attemptCount < MAX_POLLING_ATTEMPTS) { + delay(duration = 3.seconds) + attemptCount++ + val result = orderRepository.getOrderData( + userWalletId = userWallet.walletId, + orderId = orderId, + ) + val txHash = result.getOrNull()?.withdrawTxHash?.ifEmpty { null } + if (!txHash.isNullOrEmpty()) { + finalizeWithdraw( + userWallet = userWallet, + txHash = txHash, + exchangeData = exchangeData, + orderId = orderId, + ) + pollingMutex.withLock { pollingJobs.remove(key) } + return@launch + } + } + if (attemptCount >= MAX_POLLING_ATTEMPTS) { + Timber.tag(TAG).e("Polling stopped after $attemptCount unsuccessful attempts") + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) + pollingMutex.withLock { pollingJobs.remove(key) } + } + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + Timber.tag(TAG).e(exception) + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) + pollingMutex.withLock { pollingJobs.remove(key) } + } + } + pollingJobs[key] = pollingJob + } + } + + private fun stopPolling(userWalletId: String, orderId: String) { + pollingScope.launch { + pollingMutex.withLock { + val key = PollingKey(userWalletId, orderId) + pollingJobs[key]?.cancel() + pollingJobs.remove(key) } } } @@ -140,7 +218,8 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( txHash = txHash, payInExtraId = exchangeData.payInExtraId, ).also { - tangemPayStorage.deleteWithdrawOrder(userWallet.walletId, orderId) + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) + stopPolling(userWalletId = userWallet.walletId.stringValue, orderId = orderId) } } @@ -157,14 +236,12 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( override suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet) { tangemPayStorage.getWithdrawOrders(userWalletId = userWallet.walletId)?.forEach { state -> - withdrawPollingScope.launch { - try { - pollWithdrawOrderIfNeeds(userWallet = userWallet, data = state) - } catch (exception: CancellationException) { - throw exception - } catch (exception: Exception) { - Timber.tag(TAG).e(exception) - } + try { + pollWithdrawOrderIfNeeds(userWallet = userWallet, data = state) + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + Timber.tag(TAG).e(exception) } } } @@ -189,51 +266,7 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( if (!txHash.isNullOrEmpty()) { finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData, orderId = orderId) } else { - startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData) - } - return - } - - private suspend fun startWithdrawOrderPolling( - userWallet: UserWallet, - orderId: String, - exchangeData: TangemPayWithdrawExchangeState, - ) { - withdrawPollingMutex.withLock { - if (withdrawPollingJobs.containsKey(orderId)) return - - val pollingJob = withdrawPollingScope.launch { - try { - while (isActive) { - delay(duration = 5.seconds) - - orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId) - .onRight { order -> - val txHash = order.withdrawTxHash - if (txHash.isNullOrEmpty()) return@onRight - finalizeWithdraw( - userWallet = userWallet, - txHash = txHash, - exchangeData = exchangeData, - orderId = orderId, - ) - withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } - return@launch - } - .onLeft { error -> - Timber.tag(TAG).e("getOrderData error ${error.errorCode}") - withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } - return@launch - } - } - } catch (exception: CancellationException) { - throw exception - } catch (exception: Exception) { - Timber.tag(TAG).e(exception) - withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } - } - } - withdrawPollingJobs[orderId] = pollingJob + startPollingForOrder(userWallet, orderId, exchangeData) } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index c9e17d71f5..4ada6b4b01 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -125,7 +125,6 @@ internal class TangemPayDetailsModel @Inject constructor( modelScope.launch { expressTransactionsEventListener.send(ExpressTransactionsEvent.Update) } - subscribeToWithdrawOrder() } fun onPause() { @@ -150,14 +149,6 @@ internal class TangemPayDetailsModel @Inject constructor( .launchIn(modelScope) } - private fun subscribeToWithdrawOrder() { - modelScope.launch { - val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() - ?: return@launch - tangemPayWithdrawRepository.pollWithdrawOrdersIfNeeds(userWallet) - } - } - override fun onClickPinCode() { analytics.send(TangemPayAnalyticsEvents.PinCodeClicked()) if (!params.config.isPinSet) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index 1dcc11ff2a..04f33d8338 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -6,6 +6,7 @@ import com.tangem.domain.pay.model.MainCustomerInfoContentState import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.model.TangemPayCustomerInfoError import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.pay.repository.TangemPayWithdrawRepository import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -19,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber @Suppress("LongParameterList") @@ -28,10 +30,14 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( private val clickIntents: WalletClickIntents, private val cardDetailsRepository: TangemPayCardDetailsRepository, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, + private val tangemPayWithdrawRepository: TangemPayWithdrawRepository, private val analytics: WalletTangemPayAnalyticsEventSender, ) : WalletSubscriber() { override fun create(coroutineScope: CoroutineScope): Flow<*> { + coroutineScope.launch { + tangemPayWithdrawRepository.pollWithdrawOrdersIfNeeds(userWallet) + } return subscribeOnTangemPayInfoUpdates() } From d46a05479b2b9e671474ba9524d4cd2c521a8604 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Mar 2026 14:34:15 +0300 Subject: [PATCH 20/25] Updated on 2026-08-14 --- .../common/preview/WalletScreenPreviewDataLegacy.kt | 2 +- .../state/transformers/converter/TokenListStateConverter.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt index 3f4ca9070d..5485587450 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt @@ -138,7 +138,7 @@ internal object WalletScreenPreviewDataLegacy { TokensListItemUM.Portfolio( content = PortfolioItemContentUM.Empty( action = PortfolioItemContentUM.Empty.Action( - text = stringReference("Manage tokens"), + text = resourceReference(id = R.string.onboarding_add_tokens), onClick = {}, ), ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index c33c75e00e..1ad54a1a82 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -142,7 +142,7 @@ internal class TokenListStateConverter( is WalletTokensListState.Empty -> listOf() } val onEmptyAction = PortfolioItemContentUM.Empty.Action( - text = resourceReference(id = R.string.main_manage_tokens), + text = resourceReference(id = R.string.onboarding_add_tokens), onClick = { clickIntents.onManageTokensClick(account.accountId) }, ) return TokensListPortfolioItemConverter( From 5b7dbb0e2491fdffa87eadac65e6cb94a07c19fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Mar 2026 15:59:11 +0100 Subject: [PATCH 21/25] 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 42d7ab858c..4d426f621c 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.35-1442" +tangemBlockchainSdk = "releases-5.35-1449" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.35-589" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 8f9e59a762bea5f7bdc72f02609b8b645063c6ed Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 18 Mar 2026 12:40:56 +0300 Subject: [PATCH 22/25] 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 b6a00beda3..f98056e6bf 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "releases-5.35-1449" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.35-593" +tangemCardSdk = "releases-5.35-596" #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 2c2fcd27587f2c4f09b676ac121bd7b45c105886 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 18 Mar 2026 13:41:46 +0400 Subject: [PATCH 23/25] Updated on 2026-08-14 --- .../BlockchainProvidersResponseLoader.kt | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt index a6765c196a..4c04ab93ee 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt @@ -1,5 +1,6 @@ package com.tangem.blockchainsdk.providers +import android.os.Build import com.tangem.blockchainsdk.BlockchainProvidersResponse import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage @@ -28,7 +29,21 @@ internal class BlockchainProvidersResponseLoader @Inject constructor( /** Load [BlockchainProvidersResponse] */ suspend fun load(): BlockchainProvidersResponse? { - val localResponse = loadLocal().ifEmpty { return null } + val localResponse = loadLocal() + + /** + * Behavior when local config is empty or cannot be parsed: + * - Android 15 -> return null (do not load remote) + * - Android 16 (stable) -> return null (do not load remote) + * - Android 16 (preview)-> continue (attempt to load remote and merge) + * - Android 17+ -> continue (attempt to load remote and merge) + */ + val shouldLoadRemoteIfError = Build.VERSION.SDK_INT > ANDROID_16_SDK_VERSION || + Build.VERSION.SDK_INT == ANDROID_16_SDK_VERSION && Build.VERSION.PREVIEW_SDK_INT > 0 + + if (localResponse.isEmpty() && !shouldLoadRemoteIfError) { + return null + } return loadRemote().fold( onSuccess = { remoteResponse -> @@ -39,7 +54,7 @@ internal class BlockchainProvidersResponseLoader @Inject constructor( }, onFailure = { throwable -> Timber.e(throwable, "Failed to load blockchain provider types from backend") - localResponse + localResponse.ifEmpty { null } }, ) } @@ -50,4 +65,9 @@ internal class BlockchainProvidersResponseLoader @Inject constructor( dispatcher = dispatchers.io, block = tangemTechApi::getBlockchainProviders, ) + + private companion object { + // TODO Replace with Build.VERSION_CODES + const val ANDROID_16_SDK_VERSION = 36 + } } \ No newline at end of file From 64693f66caf5f025f1cb2e6bee11b7105ef1a9a2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 18 Mar 2026 13:48:33 +0300 Subject: [PATCH 24/25] Updated on 2026-08-14 --- .../di/WalletConnectDataModule.kt | 5 +- .../sessions/DefaultWcSessionsManager.kt | 54 +++++++++++++------ .../utils/WcNetworksConverter.kt | 7 --- .../supplier/MultiAccountListSupplier.kt | 10 ++++ 4 files changed, 51 insertions(+), 25 deletions(-) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index b0012d93a3..ef493b58ff 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -22,7 +22,6 @@ import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.supplier.MultiAccountListSupplier -import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.WcRequestService @@ -92,6 +91,7 @@ internal object WalletConnectDataModule { dispatchers: CoroutineDispatcherProvider, getWallets: GetWalletsUseCase, wcNetworksConverter: WcNetworksConverter, + multiAccountListSupplier: MultiAccountListSupplier, analytics: AnalyticsEventHandler, wcScope: WcScope, ): DefaultWcSessionsManager { @@ -102,6 +102,7 @@ internal object WalletConnectDataModule { wcNetworksConverter = wcNetworksConverter, analytics = analytics, scope = wcScope, + multiAccountListSupplier = multiAccountListSupplier, ) } @@ -181,12 +182,10 @@ internal object WalletConnectDataModule { namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>, walletManagersFacade: WalletManagersFacade, singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - singleAccountSupplier: SingleAccountSupplier, ): WcNetworksConverter = WcNetworksConverter( namespaceConverters = namespaceConverters, walletManagersFacade = walletManagersFacade, singleAccountStatusListSupplier = singleAccountStatusListSupplier, - singleAccountSupplier = singleAccountSupplier, ) @Provides diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 96a3982807..c6b1625126 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -6,10 +6,17 @@ import arrow.core.right import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.data.walletconnect.utils.* +import com.tangem.data.walletconnect.utils.WC_TAG +import com.tangem.data.walletconnect.utils.WcNetworksConverter +import com.tangem.data.walletconnect.utils.WcScope +import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.datasource.local.walletconnect.WalletConnectStore +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.models.account.Account import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionDTO @@ -28,7 +35,8 @@ import kotlin.coroutines.resume @Suppress("LongParameterList") internal class DefaultWcSessionsManager( private val store: WalletConnectStore, - private val getWallets: GetWalletsUseCase, + getWallets: GetWalletsUseCase, + multiAccountListSupplier: MultiAccountListSupplier, private val dispatchers: CoroutineDispatcherProvider, private val wcNetworksConverter: WcNetworksConverter, private val analytics: AnalyticsEventHandler, @@ -37,18 +45,31 @@ internal class DefaultWcSessionsManager( private val onSessionDelete = Channel(capacity = Channel.BUFFERED) - override val sessions: Flow>> - get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore } - .transform { pair -> - val (wallets, inStore) = pair - val inSdk: List = WalletKit.getListOfActiveSessions() - val associatedSessions: List = associate(inSdk, inStore, wallets) - val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions) - if (someRemove) return@transform // ignore emit, wait next one - emit(associatedSessions.groupBy { it.wallet }) - } - .distinctUntilChanged() - .flowOn(dispatchers.io) + override val sessions: Flow>> = combine( + flow = getWallets(), + flow2 = store.sessions, + flow3 = multiAccountListSupplier.invokeMap(), + transform = ::Triple, + ) + .transformLatest { triple -> + val (wallets, inStore, allWalletsAccounts) = triple + val inSdk: List = WalletKit.getListOfActiveSessions() + val associatedSessions: List = associate( + inSdk = inSdk, + inStore = inStore, + wallets = wallets, + allWalletsAccounts = allWalletsAccounts, + ) + removeUnknownSessions(inStore, inSdk, associatedSessions) + emit(associatedSessions.groupBy { it.wallet }) + } + .distinctUntilChanged() + .flowOn(dispatchers.io) + .shareIn( + scope = scope, + started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 0, replayExpirationMillis = 0), + replay = 1, + ) override fun onWcSdkInit() { listenOnSessionDelete() @@ -78,6 +99,7 @@ internal class DefaultWcSessionsManager( inSdk: List, inStore: Set, wallets: List, + allWalletsAccounts: LinkedHashMap, ): List { // if the WcSdk `onSessionSettleResponse` callback arrives late, merge pending approvals with WcSdk sessions val savedPending = store.pendingApproval.first() @@ -91,7 +113,9 @@ internal class DefaultWcSessionsManager( val wcSessions = savedPending.plus(inStore).mapNotNull { storeSession -> val wallet = wallets.find { it.walletId == storeSession.walletId } ?: return@mapNotNull null val sdkSession = inSdk.find { it.topic == storeSession.topic } ?: return@mapNotNull null - val account = wcNetworksConverter.getAccount(storeSession.accountId) as? Account.CryptoPortfolio + val walletAccounts = allWalletsAccounts[storeSession.walletId] ?: return@mapNotNull null + val account = walletAccounts.accounts + .find { account -> account.accountId == storeSession.accountId } as? Account.CryptoPortfolio ?: return@mapNotNull null val networks = wcNetworksConverter.findWalletNetworks(wallet, account, sdkSession) val originUrl = storeSession.url ?: sdkSession.metaData?.url ?: "" diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index 9104712d62..8ef4d7a69e 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -6,10 +6,8 @@ import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.common.currency.isCustomCoin import com.tangem.data.walletconnect.model.CAIP10 -import com.tangem.domain.account.producer.SingleAccountProducer import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus @@ -28,7 +26,6 @@ internal class WcNetworksConverter @Inject constructor( private val namespaceConverters: Set, private val walletManagersFacade: WalletManagersFacade, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val singleAccountSupplier: SingleAccountSupplier, ) { fun createNetwork(chainId: String, wallet: UserWallet): Network? { @@ -117,10 +114,6 @@ internal class WcNetworksConverter @Inject constructor( return existNetworks } - suspend fun getAccount(accountId: AccountId): Account? { - return singleAccountSupplier.getSyncOrNull(SingleAccountProducer.Params(accountId)) - } - suspend fun convertNetworksForApprove(sessionForApprove: WcSessionApprove): List { val portfolioNetworks = getAccountNetworks(sessionForApprove.account.accountId) return sessionForApprove.network diff --git a/domain/account/src/main/java/com/tangem/domain/account/supplier/MultiAccountListSupplier.kt b/domain/account/src/main/java/com/tangem/domain/account/supplier/MultiAccountListSupplier.kt index 45989af478..a914d2b6f4 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/supplier/MultiAccountListSupplier.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/supplier/MultiAccountListSupplier.kt @@ -3,7 +3,9 @@ package com.tangem.domain.account.supplier import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.MultiAccountListProducer import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** * Supplier that provides a list of [AccountList]s for all user wallets. @@ -18,4 +20,12 @@ abstract class MultiAccountListSupplier( operator fun invoke(): Flow> { return super.invoke(params = Unit) } + + fun invokeMap(): Flow> = invoke() + .map { accountLists -> + accountLists.associateByTo( + destination = linkedMapOf(), + keySelector = { accountList -> accountList.userWalletId }, + ) + } } \ No newline at end of file From 3be81a446d00c729b27c1240d6f7a45574c7d8b8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 18 Mar 2026 10:48:54 +0000 Subject: [PATCH 25/25] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index f98056e6bf..3e5fe1fd8a 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.35-1449" +tangemBlockchainSdk = "develop-1447" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.35-596" +tangemCardSdk = "develop-582" #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 ^ -tangemHotSdk = "develop-547" +tangemHotSdk = "develop-539" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^