From db08399066a8950f9a98071521919568b66848c3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Aug 2025 14:16:56 +0700 Subject: [PATCH 01/10] Updated on 2026-08-14 --- .../connections/components/WcPairComponent.kt | 26 ++++++++-------- .../connections/model/WcPairModel.kt | 18 +++++------ .../connections/routes/WcAppInfoRoutes.kt | 31 ++++++++++--------- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt index afa23ea711..80430447de 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt @@ -57,9 +57,9 @@ internal class WcPairComponent( private fun onChildBack() { when (val config = contentStack.value.active.configuration) { is WcAppInfoRoutes.AppInfo -> dismiss() - is Alert -> when (config.alertType) { - is Alert.Type.UnsupportedDApp, - is Alert.Type.UnsupportedNetwork, + is Alert -> when (config) { + is Alert.UnsupportedDApp, + is Alert.UnsupportedNetwork, -> dismiss() else -> model.stackNavigation.pop() } @@ -85,7 +85,7 @@ internal class WcPairComponent( ) is Alert -> AlertsComponentV2( appComponentContext = appComponentContext, - messageUM = createBottomSheetMessageUM(config.alertType), + messageUM = createBottomSheetMessageUM(config), ) is WcAppInfoRoutes.SelectNetworks -> WcSelectNetworksComponent( appComponentContext = appComponentContext, @@ -110,18 +110,18 @@ internal class WcPairComponent( } } - private fun createBottomSheetMessageUM(alertType: Alert.Type): MessageBottomSheetUMV2 { + private fun createBottomSheetMessageUM(alertType: Alert): MessageBottomSheetUMV2 { return when (alertType) { - is Alert.Type.Verified -> WcAlertsFactory.createVerifiedDomainAlert(alertType.appName) - is Alert.Type.UnknownDomain -> WcAlertsFactory.createUnknownDomainAlert(model::connectFromAlert) - is Alert.Type.InvalidDomain -> WcAlertsFactory.createInvalidDomainAlert(model::errorAlertOnDismiss) - is Alert.Type.UnsafeDomain -> WcAlertsFactory.createUnsafeDomainAlert(model::connectFromAlert) - is Alert.Type.UnsupportedDApp -> + is Alert.Verified -> WcAlertsFactory.createVerifiedDomainAlert(alertType.appName) + is Alert.UnknownDomain -> WcAlertsFactory.createUnknownDomainAlert(model::connectFromAlert) + is Alert.InvalidDomain -> WcAlertsFactory.createInvalidDomainAlert(model::errorAlertOnDismiss) + is Alert.UnsafeDomain -> WcAlertsFactory.createUnsafeDomainAlert(model::connectFromAlert) + is Alert.UnsupportedDApp -> WcAlertsFactory.createUnsupportedDomainAlert(alertType.appName, model::errorAlertOnDismiss) - is Alert.Type.UnsupportedNetwork -> + is Alert.UnsupportedNetwork -> WcAlertsFactory.createUnsupportedChainAlert(alertType.appName, model::errorAlertOnDismiss) - is Alert.Type.UriAlreadyUsed -> WcAlertsFactory.createUriAlreadyUsedAlert(model::errorAlertOnDismiss) - is Alert.Type.TimeoutException -> WcAlertsFactory.createTimeoutExceptionAlert(model::errorAlertOnDismiss) + is Alert.UriAlreadyUsed -> WcAlertsFactory.createUriAlreadyUsedAlert(model::errorAlertOnDismiss) + is Alert.TimeoutException -> WcAlertsFactory.createTimeoutExceptionAlert(model::errorAlertOnDismiss) } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index fe64931b39..7449212ae2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -186,7 +186,7 @@ internal class WcPairModel @Inject constructor( source = WcAnalyticEvents.NoticeSecurityAlert.Source.Domain, ) analytics.send(event) - stackNavigation.pushNew(WcAppInfoRoutes.Alert(WcAppInfoRoutes.Alert.Type.UnknownDomain)) + stackNavigation.pushNew(WcAppInfoRoutes.Alert.UnknownDomain) } private fun showSecurityRiskAlert() { @@ -196,11 +196,11 @@ internal class WcPairModel @Inject constructor( source = WcAnalyticEvents.NoticeSecurityAlert.Source.Domain, ) analytics.send(event) - stackNavigation.pushNew(WcAppInfoRoutes.Alert(WcAppInfoRoutes.Alert.Type.UnsafeDomain)) + stackNavigation.pushNew(WcAppInfoRoutes.Alert.UnsafeDomain) } private fun showVerifiedAlert(appName: String) { - stackNavigation.pushNew(WcAppInfoRoutes.Alert(WcAppInfoRoutes.Alert.Type.Verified(appName))) + stackNavigation.pushNew(WcAppInfoRoutes.Alert.Verified(appName)) } private fun processSuccessfullyConnected(session: WcSession) { @@ -216,18 +216,18 @@ internal class WcPairModel @Inject constructor( private fun processError(error: WcPairError) { val alert = when (error) { - is WcPairError.InvalidDomainURL -> WcAppInfoRoutes.Alert.Type.InvalidDomain - is WcPairError.UnsupportedDApp -> WcAppInfoRoutes.Alert.Type.UnsupportedDApp(error.appName) - is WcPairError.UnsupportedBlockchains -> WcAppInfoRoutes.Alert.Type.UnsupportedNetwork(error.appName) - is WcPairError.UriAlreadyUsed -> WcAppInfoRoutes.Alert.Type.UriAlreadyUsed - is WcPairError.TimeoutException -> WcAppInfoRoutes.Alert.Type.TimeoutException + is WcPairError.InvalidDomainURL -> WcAppInfoRoutes.Alert.InvalidDomain + is WcPairError.UnsupportedDApp -> WcAppInfoRoutes.Alert.UnsupportedDApp(error.appName) + is WcPairError.UnsupportedBlockchains -> WcAppInfoRoutes.Alert.UnsupportedNetwork(error.appName) + is WcPairError.UriAlreadyUsed -> WcAppInfoRoutes.Alert.UriAlreadyUsed + is WcPairError.TimeoutException -> WcAppInfoRoutes.Alert.TimeoutException else -> { messageSender.send(ToastMessage(message = stringReference(error.message))) router.pop() null } } - alert?.let { stackNavigation.pushNew(WcAppInfoRoutes.Alert(it)) } + alert?.let { stackNavigation.pushNew(it) } } override fun onWalletSelected(userWalletId: UserWalletId) { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt index 784e0457ed..c83d3cd1a1 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt @@ -2,14 +2,13 @@ package com.tangem.features.walletconnect.connections.routes import androidx.compose.runtime.Immutable import com.tangem.core.decompose.navigation.Route -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable @Serializable @Immutable -internal sealed class WcAppInfoRoutes : TangemBottomSheetConfigContent, Route { +internal sealed class WcAppInfoRoutes : Route { @Serializable data object AppInfo : WcAppInfoRoutes() @@ -26,17 +25,21 @@ internal sealed class WcAppInfoRoutes : TangemBottomSheetConfigContent, Route { ) : WcAppInfoRoutes() @Serializable - data class Alert(val alertType: Type) : WcAppInfoRoutes() { - @Serializable - sealed class Type { - data class Verified(val appName: String) : Type() - data object UnknownDomain : Type() - data object UnsafeDomain : Type() - data object InvalidDomain : Type() - data class UnsupportedDApp(val appName: String) : Type() - data class UnsupportedNetwork(val appName: String) : Type() - data object UriAlreadyUsed : Type() - data object TimeoutException : Type() - } + sealed class Alert : WcAppInfoRoutes() { + @Serializable data class Verified(val appName: String) : Alert() + + @Serializable data object UnknownDomain : Alert() + + @Serializable data object UnsafeDomain : Alert() + + @Serializable data object InvalidDomain : Alert() + + @Serializable data class UnsupportedDApp(val appName: String) : Alert() + + @Serializable data class UnsupportedNetwork(val appName: String) : Alert() + + @Serializable data object UriAlreadyUsed : Alert() + + @Serializable data object TimeoutException : Alert() } } \ No newline at end of file From ae809ece30ac3aa761e5bc042eea518f222a07d1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 24 Aug 2025 14:16:45 +0500 Subject: [PATCH 02/10] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 11 + .../tap/routing/utils/DeepLinkFactory.kt | 3 + .../tap/routing/utils/DeepLinkFactoryTest.kt | 33 +- .../tangem/common/routing/DeepLinkRoute.kt | 4 + .../common/routing/deeplink/DeeplinkConst.kt | 1 + .../api/tangemTech/TangemTechApi.kt | 5 + .../models/PromocodeActivationBody.kt | 10 + .../models/PromocodeActivationResponse.kt | 9 + core/res/src/main/res/values-es/strings.xml | 8 +- core/res/src/main/res/values-ja/strings.xml | 12 +- core/res/src/main/res/values-ru/strings.xml | 10 + .../src/main/res/values-uk-rUA/strings.xml | 10 + core/res/src/main/res/values/strings.xml | 40 +++ .../com/tangem/core/ui/extensions/String.kt | 25 ++ .../tangem/core/ui/message/EventMessage.kt | 2 + .../core/ui/message/EventMessageEffect.kt | 47 +++ .../core/ui/extensions/StringMaskTest.kt | 91 +++++ .../data/wallets/DefaultWalletsRepository.kt | 30 ++ .../wallets/DefaultWalletsRepositoryTest.kt | 64 ++++ .../wallets/PromoCodeActivationResult.kt | 9 + .../models/errors/ActivatePromoCodeError.kt | 12 + .../wallets/repository/WalletsRepository.kt | 4 + .../ActivateBitcoinPromocodeUseCase.kt | 14 + .../wallet/deeplink/PromoDeeplinkHandler.kt | 10 + features/wallet/impl/build.gradle.kts | 6 +- .../deeplink/DefaultPromoDeeplinkHandler.kt | 142 ++++++++ .../deeplink/di/WalletDeepLinkModule.kt | 6 + .../DefaultPromoDeeplinkHandlerTest.kt | 331 ++++++++++++++++++ 28 files changed, 939 insertions(+), 10 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationBody.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationResponse.kt create mode 100644 core/ui/src/test/java/com/tangem/core/ui/extensions/StringMaskTest.kt create mode 100644 domain/wallets/models/src/main/java/com/tangem/domain/wallets/PromoCodeActivationResult.kt create mode 100644 domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/errors/ActivatePromoCodeError.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ActivateBitcoinPromocodeUseCase.kt create mode 100644 features/wallet/api/src/main/kotlin/com/tangem/features/wallet/deeplink/PromoDeeplinkHandler.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt create mode 100644 features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2c1f4e5fe..9ba22faa95 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -248,6 +248,17 @@ android:host="swap" android:scheme="tangem" /> + + + + + + + + + diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index 436d1a2ef3..e5c704d74a 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -15,6 +15,7 @@ import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler +import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLinkHandler import com.tangem.utils.coroutines.JobHolder @@ -46,6 +47,7 @@ internal class DeepLinkFactory @Inject constructor( private val buyDeepLink: BuyDeepLinkHandler.Factory, private val sellDeepLink: SellDeepLinkHandler.Factory, private val swapDeepLink: SwapDeepLinkHandler.Factory, + private val promoDeepLink: PromoDeeplinkHandler.Factory, ) { private val permittedAppRoute = MutableStateFlow(false) @@ -131,6 +133,7 @@ internal class DeepLinkFactory @Inject constructor( DeepLinkRoute.Sell.host -> sellDeepLink.create() DeepLinkRoute.Swap.host -> swapDeepLink.create() DeepLinkRoute.WalletConnect.host -> walletConnectDeepLink.create(deeplinkUri) + DeepLinkRoute.Promo.host -> promoDeepLink.create(coroutineScope, queryParams) else -> { Timber.i( """ diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index 34c9ae969c..dde88e9295 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -13,6 +13,7 @@ import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler +import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLinkHandler import io.mockk.every @@ -68,6 +69,10 @@ class DeepLinkFactoryTest { every { create() } returns mockk() } + private val promoDeepLinkFactory = mockk(relaxed = true) { + every { create(any(), any()) } returns mockk() + } + private val cardSdkProvider = mockk(relaxed = true) { every { sdk.uiVisibility() } returns MutableStateFlow(false) } @@ -91,6 +96,7 @@ class DeepLinkFactoryTest { buyDeepLink = buyDeepLinkFactory, sellDeepLink = sellDeepLinkFactory, swapDeepLink = swapDeepLinkFactory, + promoDeepLink = promoDeepLinkFactory, ) @OptIn(ExperimentalCoroutinesApi::class) @@ -303,13 +309,19 @@ class DeepLinkFactoryTest { every { mockedUri.host } returns "swap" deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() - verify { sellDeepLinkFactory.create() } + verify { swapDeepLinkFactory.create() } // Test Buy every { mockedUri.host } returns "buy" deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() verify { buyDeepLinkFactory.create() } + + // Test Promo + every { mockedUri.host } returns "promo" + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) + advanceUntilIdle() + verify { promoDeepLinkFactory.create(eq(testScope), eq(emptyMap())) } } @Test @@ -332,6 +344,7 @@ class DeepLinkFactoryTest { buyDeepLinkFactory.create() sellDeepLinkFactory.create() swapDeepLinkFactory.create() + promoDeepLinkFactory.create(any(), any()) } } @@ -400,4 +413,22 @@ class DeepLinkFactoryTest { advanceUntilIdle() verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) } } + + @Test + fun `handleTangemDeepLinks routes to promo handler`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.host } returns "promo" + every { mockedUri.query } returns "promo_code=PROMO123" + every { mockedUri.queryParameterNames } returns setOf("promo_code") + every { mockedUri.getQueryParameter("promo_code") } returns "PROMO123" + + deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) + + advanceUntilIdle() + + verify { + promoDeepLinkFactory.create(eq(testScope), eq(mapOf("promo_code" to "PROMO123"))) + } + } } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt index 78a4ad85ea..1fbffcb8a8 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt @@ -55,6 +55,10 @@ sealed class DeepLinkRoute { data object WalletConnect : DeepLinkRoute() { override val host: String = "wc" } + + data object Promo : DeepLinkRoute() { + override val host: String = "promo" + } } enum class DeepLinkScheme(val scheme: String) { diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt index a435e90549..575be52e80 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt @@ -11,5 +11,6 @@ object DeeplinkConst { const val TOKEN_ID_KEY = "token_id" const val DERIVATION_PATH_KEY = "derivation_path" const val TRANSACTION_ID_KEY = "transaction_id" + const val PROMO_CODE_KEY = "promo_code" const val NAME_KEY = "name" } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 14ec9f906f..2d6d08aff3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -176,4 +176,9 @@ interface TangemTechApi { @GET("v1/user-wallets/wallets/by-app/{app_id}") suspend fun getWallets(@Path("app_id") appId: String): ApiResponse> // endregion + + // promo + @POST("promo/v1/promo-codes/activate") + suspend fun activatePromoCode(@Body body: PromocodeActivationBody): ApiResponse + // endregion } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationBody.kt new file mode 100644 index 0000000000..9d28f527e7 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationBody.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class PromocodeActivationBody( + @Json(name = "promoCode") val promoCode: String, + @Json(name = "address") val address: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationResponse.kt new file mode 100644 index 0000000000..427a4f78d9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class PromocodeActivationResponse( + @Json(name = "status") val status: String, +) \ No newline at end of file diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index fa2411cdf5..37b3938536 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -202,7 +202,7 @@ NFT No Ninguna dirección - No aregada + No agregada Ahora no Ahora OK @@ -250,8 +250,8 @@ Condiciones de uso Hoy - %d ficha - %d fichas + %d token + %d tokens Transacción fallida Estado de la transacción @@ -1348,7 +1348,7 @@ Para continuar, vuelva a conectar su sesión de dApp con la red requerida %s. Red no conectada Verifique su conexión de red - Tiempo de espera de la solicitud agotado + Tiempo de espera agotado Vuelva a su navegador y vuelva a conectarse a través de WalletConnect. La sesión de Wallet Connect se desconectó Firmar de todos modos diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 5ffb365ef3..4c5d34d274 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -12,6 +12,7 @@ ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 アクセスコードの作成 アクセスコード + アーカイブされたアカウント 回復する アーカイブ済み アカウントをアーカイブする @@ -26,6 +27,7 @@ 新しいアカウント アカウントを追加 アカウントを編集 + アカウントを長押しして並べ替える 編集を続ける 破棄 新しいアカウントを破棄してもよろしいですか? @@ -71,10 +73,8 @@ 設定に移動して、Tangemアプリで生体認証を有効にします。 生体認証を有効にする %1$sが無効になると、アプリのロックを解除してウォレットを操作するために、パスコードを入力する必要があります。 - 後でウォレットのアクセスコードを入力してもらいます。それを安全に保存し、今後の利用に備えるためです。 これにより、保存されているウォレットアクセスコードがすべて削除されます。ウォレットでの今後の操作には、アクセスコードの送信が必要になります。 保存したデバイスを削除すると、保存されているすべてのウォレットとそのアクセスコードがアプリから削除されます。 - これにより、保存されているウォレットアクセスコードがすべて削除されます。今後ウォレットを操作するには、アクセスコードの送信が必要になります。 アクセスコードを要求する このオプションを選択すると、機密性の高い操作における生体認証が無効になります。取引の署名時などには、毎回アクセスコードの入力が必要になります。 アクセスコードを保存 @@ -116,6 +116,12 @@ 30秒後に再試行するか、カードまたはリングをスキャンしてください 試行回数が多すぎます お使いの携帯電話で生体認証が無効になっているため、アプリにウォレットを保存できません。ウォレットを保存するには、携帯電話の設定で生体認証機能を有効にしてください。 + プロモーションコードの処理中にエラーが発生しました。しばらくしてからもう一度お試しください。 + プロモーションコードが正常に有効化されました。14日以内に10 USDT相当のビットコインボーナスが付与されます。 + プロモーションコードが有効になりました + このプロモーションコードは既に使用されているため、再度使うことはできません。 + このプロモーションコードは無効であり、有効化できません。 + ボーナスを受け取るにはビットコインアドレスが必要です。ウォレットにビットコインアドレスを追加し、再度アクティベーションをお試しください。 バックアップ処理を開始する 銀行カードまたはその他の支払い方法を使用する @@ -149,6 +155,7 @@ ADAが不足しています。 受け入れる アクセスが拒否されました + アカウント 追加 ポートフォリオに追加 トークンを追加 @@ -489,7 +496,6 @@ 最新の機能とニュースをお届けします シードフレーズのバックアップ モバイルウォレットを作成する - このリカバリーフレーズはすでにインポートされています。 モバイルウォレット この情報はAIで生成されました。 \nエラーが見つかった場合は、ここをタップしてください。 アクセスコードを変更するには、上図のようにカードまたはリングをタップし、操作が終了するまで取り外さないでください。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index befe0fd2d7..899b661b01 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -59,6 +59,16 @@ Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту или кольцо Слишком много попыток Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона. + При обработке промокода произошла ошибка. Пожалуйста, попробуйте позже. + Ошибка активации + Ваш промокод успешно активирован. Бонус 10 USDT в Bitcoin будет зачислен через 14 дней. + Промокод активирован + Этот промокод уже был использован и не может быть активирован повторно. + Код недоступен + Этот промокод недействителен и не может быть активирован. + Неверный код + Для зачисления бонуса нужен Bitcoin-адрес. Добавьте его в портфель и повторите активацию. + Требуется Bitcoin-адрес Начать резервное копирование Используйте банковскую карту или другие методы оплаты diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 5c7d9f4ef4..21880f895f 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -59,6 +59,16 @@ Будь ласка, спробуйте знову через 30 секунд або відскануйте картку або кільце Забагато спроб Ви вимкнули біометричну автентифікацію на своєму телефоні і не зможете зберігати гаманці в додатку. Щоб зберегти гаманці, будь ласка, увімкніть функцію біометричної автентифікації в налаштуваннях телефону. + Під час обробки промокоду сталася помилка. Будь ласка, спробуйте пізніше. + Помилка активації + Ваш промокод успішно активовано. Бонус 10 USDT у Bitcoin буде зараховано через 14 днів. + Промокод активовано + Цей промокод уже був використаний і не може бути активований повторно. + Код недоступний + Цей промокод недійсний і не може бути активований. + Невірний код + Для зарахування бонусу потрібна Bitcoin-адреса. Додайте її до портфеля та повторіть активацію. + Потрібна Bitcoin-адреса Почніть процес резервного копіювання Використовуйте банківську картку або інші способи оплати diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8bda8073a5..8387e25d6a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -12,7 +12,12 @@ Set a %s-digit Access Code to unlock your wallet. Create Access Code Access code + You cannot create more than %1$s accounts. Archive one to add new. + Can’t add new account + Archived accounts Recover + You’re about to recover “%1$s”. + Recover account Archived Archive account Archive @@ -26,6 +31,8 @@ New account Add account Edit account + %1$s in %2$s + Long tap on an account to reorder accounts Keep Editing Discard Are you sure you want to discard new account? @@ -117,6 +124,16 @@ Please try again in 30 seconds or scan the card or ring Too many attempts You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. + An error occurred while processing your promo code. Please try again later. + Activation error + Your promo code has been successfully activated. A bonus of 10 USDT in Bitcoin will be credited in 14 days. + Promo code activated + This promo code has already been used and cannot be activated again. + Code unavailable + This promo code is not valid and cannot be activated. + Invalid code + A Bitcoin address is required to receive the bonus. Please add one to your wallet and retry the activation. + Bitcoin address required Start backup process Use a bank card or other payment methods @@ -152,6 +169,7 @@ Not enough ADA Accept Access denied + Accounts Add Add to portfolio Add token @@ -239,6 +257,10 @@ month Network fee Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level + + %d network + %d networks + Next NFT No @@ -485,6 +507,8 @@ If you do, you\'ll need to start over. Recover an existing wallet stored in your Google Drive backup Google Drive Backup + Upgrade your security right away with a best in class hardware wallet from Tangem. + Hardware Wallet Go to backup To secure your wallet with a Access Code, complete the backup first. Finish Backup First @@ -498,6 +522,19 @@ Create Mobile Wallet This recovery phrase has already been imported Mobile Wallet + Your funds stay safe and fully accessible during the process + Funds access + All private wallet data will be removed from the mobile app and stored securely on your Tangem device only + General Security + Private keys will be moved from the app to your Tangem card or ring + Key Migration + Scan device + Start upgrade + You’re about to upgrade your wallet to Tangem Wallet. This will keep your assets safe with cold storage. + Tangem Wallet + Upgrade to Hardware Wallet + Keep your crypto safe with Tangem’s best-in-class hardware wallet. + Upgrade wallet with a hardware
backup This information was generated with AI.\nTap here, if you find any errors. To change the access code tap the card or ring as shown above and do not remove until the end of the operation To change the passcode tap the card as shown above and do not remove until the end of the operation @@ -702,6 +739,9 @@ Join Now Share your code - earn 5 USDT per sale. Your friend gets 10% OFF. Get REWARDS for every friend! + Buy crypto + Enjoy zero fees when purchasing crypto via SEPA transfer. + Buy Crypto with SEPA Set up a single access code to protect all your devices. Protect Set an individual access code for each card or ring later. diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt index f60730bd69..3d5ce5fc09 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt @@ -80,4 +80,29 @@ fun String.capitalize(): String = replaceFirstChar { if (it.isLowerCase()) it.ti fun String.orMaskWithStars(maskWithStars: Boolean): String { return if (maskWithStars) THREE_STARS else this +} + +/** + * Returns a masked representation of the string for safe display. + * + * Rules: + * - empty string: returned unchanged + * - length <= 2: all characters are replaced with '*' + * - length <= 4: keep the first and last characters, mask the middle + * - length > 4: keep the first two and last two characters, mask the middle with "**" + * + * @return masked string according to the rules above + */ +@Suppress("MagicNumber") +fun String.mask(): String { + return when { + this.isEmpty() -> this + this.length <= 2 -> "*".repeat(this.length) // mask all if too short + this.length <= 4 -> this.first() + "*".repeat(this.length - 2) + this.last() + else -> { + val prefix = this.take(2) + val suffix = this.takeLast(2) + "$prefix**$suffix" + } + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt index 216e6c755c..d2988d0b0c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt @@ -147,6 +147,8 @@ data class DialogMessage( } } +data class GlobalLoadingMessage(val isShow: Boolean) : EventMessage + /** * Shows a bottom sheet. * diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageEffect.kt index 25984f4a44..8d23185045 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageEffect.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageEffect.kt @@ -2,13 +2,23 @@ package com.tangem.core.ui.message import android.content.Context import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM @@ -17,6 +27,7 @@ import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.LocalEventMessageHandler import com.tangem.core.ui.res.LocalSnackbarHostState +import com.tangem.core.ui.res.TangemTheme @Composable fun EventMessageEffect( @@ -33,6 +44,7 @@ fun EventMessageEffect( var dialogMessage: DialogMessage? by remember { mutableStateOf(value = null) } var bottomSheetMessage: BottomSheetMessage? by remember { mutableStateOf(value = null) } var bottomSheetMessageV2: BottomSheetMessageV2? by remember { mutableStateOf(value = null) } + var loadingMessage: GlobalLoadingMessage? by remember { mutableStateOf(value = null) } EventEffect(event = messageEvent) { message -> when (message) { @@ -51,6 +63,13 @@ fun EventMessageEffect( is ToastMessage -> { onShowToast(message, context) } + is GlobalLoadingMessage -> { + loadingMessage = if (message.isShow) { + message + } else { + null + } + } } } @@ -80,6 +99,34 @@ fun EventMessageEffect( onDismissRequest = { bottomSheetMessageV2 = null }, ) } + + loadingMessage?.let { + LoadingDialog() + } +} + +@Composable +private fun LoadingDialog() { + Dialog( + onDismissRequest = {}, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + ), + ) { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + SpacerHMax() + CircularProgressIndicator( + modifier = Modifier.align(Alignment.CenterHorizontally), + color = TangemTheme.colors.icon.primary1, + ) + SpacerHMax() + } + } } @Composable diff --git a/core/ui/src/test/java/com/tangem/core/ui/extensions/StringMaskTest.kt b/core/ui/src/test/java/com/tangem/core/ui/extensions/StringMaskTest.kt new file mode 100644 index 0000000000..8717d32a2c --- /dev/null +++ b/core/ui/src/test/java/com/tangem/core/ui/extensions/StringMaskTest.kt @@ -0,0 +1,91 @@ +package com.tangem.core.ui.extensions + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class StringMaskTest { + + @Test + fun GIVEN_empty_string_WHEN_mask_THEN_returns_empty_string() { + // GIVEN + val input = "" + + // WHEN + val actual = input.mask() + + // THEN + assertThat(actual).isEqualTo("") + } + + @Test + fun GIVEN_one_char_WHEN_mask_THEN_returns_asterisk() { + // GIVEN + val input = "a" + + // WHEN + val actual = input.mask() + + // THEN + assertThat(actual).isEqualTo("*") + } + + @Test + fun GIVEN_two_chars_WHEN_mask_THEN_returns_two_asterisks() { + // GIVEN + val input = "ab" + + // WHEN + val actual = input.mask() + + // THEN + assertThat(actual).isEqualTo("**") + } + + @Test + fun GIVEN_three_chars_WHEN_mask_THEN_masks_middle_only() { + // GIVEN + val input = "abc" + + // WHEN + val actual = input.mask() + + // THEN + assertThat(actual).isEqualTo("a*c") + } + + @Test + fun GIVEN_four_chars_WHEN_mask_THEN_masks_middle_two() { + // GIVEN + val input = "abcd" + + // WHEN + val actual = input.mask() + + // THEN + assertThat(actual).isEqualTo("a**d") + } + + @Test + fun GIVEN_five_chars_WHEN_mask_THEN_keeps_edges_and_two_stars_in_middle() { + // GIVEN + val input = "abcde" + + // WHEN + val actual = input.mask() + + // THEN + assertThat(actual).isEqualTo("ab**de") + } + + @Test + fun GIVEN_seven_chars_WHEN_mask_THEN_keeps_two_on_each_side_and_two_stars() { + // GIVEN + val input = "abcdefg" + + // WHEN + val actual = input.mask() + + // THEN + assertThat(actual).isEqualTo("ab**fg") + } +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 8808bd336a..da3b78db9c 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -1,12 +1,17 @@ package com.tangem.data.wallets +import arrow.core.Either +import arrow.core.left +import arrow.core.right import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter import com.tangem.data.wallets.converters.WalletIdBodyConverter import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.response.ApiResponseError.HttpException +import com.tangem.datasource.api.common.response.fold import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.MarkUserWalletWasCreatedBody +import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status import com.tangem.datasource.api.tangemTech.models.WalletBody @@ -23,6 +28,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.models.UserWalletRemoteInfo +import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.WEEK_MILLIS import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -308,4 +314,28 @@ internal class DefaultWalletsRepository( body = walletsBody, ).getOrThrow() } + + override suspend fun activatePromoCode( + promoCode: String, + bitcoinAddress: String, + ): Either = withContext(dispatchers.io) { + tangemTechApi.activatePromoCode( + body = PromocodeActivationBody( + promoCode = promoCode, + address = bitcoinAddress, + ), + ).fold({ + return@fold it.status.right() + }, { error -> + val error = when (error) { + is HttpException -> when (error.code) { + HttpException.Code.NOT_FOUND -> ActivatePromoCodeError.InvalidPromoCode + HttpException.Code.CONFLICT -> ActivatePromoCodeError.PromocodeAlreadyUsed + else -> ActivatePromoCodeError.ActivationFailed + } + else -> ActivatePromoCodeError.ActivationFailed + } + return@fold error.left() + },) + } } \ No newline at end of file diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index 2911dd5968..1a64234232 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -6,12 +6,16 @@ import com.google.common.truth.Truth.assertThat import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError.HttpException import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.WalletResponse +import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody +import com.tangem.datasource.api.tangemTech.models.PromocodeActivationResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery @@ -235,4 +239,64 @@ class DefaultWalletsRepositoryTest { ) } } + + @Test + fun `GIVEN valid data WHEN activatePromoCode THEN returns Right with status and calls API`() = runTest { + // GIVEN + val promoCode = "PROMO123" + val address = "bc1qexampleaddress" + coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Success( + PromocodeActivationResponse(status = "activated"), + ) + + // WHEN + val result = repository.activatePromoCode(promoCode = promoCode, bitcoinAddress = address) + + // THEN + var right: String? = null + var left: ActivatePromoCodeError? = null + result.fold({ left = it }, { right = it }) + assertThat(left).isNull() + assertThat(right).isEqualTo("activated") + + coVerify(exactly = 1) { + tangemTechApi.activatePromoCode( + match { it is PromocodeActivationBody && it.promoCode == promoCode && it.address == address }, + ) + } + } + + @Test + fun `GIVEN NOT_FOUND error WHEN activatePromoCode THEN returns Left InvalidPromoCode`() = runTest { + // GIVEN + coEvery { tangemTechApi.activatePromoCode(any()) } returns + ApiResponse.Error( + HttpException(code = HttpException.Code.NOT_FOUND, message = null, errorBody = null), + ) as ApiResponse + + // WHEN + val result = repository.activatePromoCode(promoCode = "PROMO", bitcoinAddress = "addr") + + // THEN + var error: ActivatePromoCodeError? = null + result.fold({ error = it }, { }) + assertThat(error).isEqualTo(ActivatePromoCodeError.InvalidPromoCode) + } + + @Test + fun `GIVEN CONFLICT error WHEN activatePromoCode THEN returns Left PromocodeAlreadyUsed`() = runTest { + // GIVEN + coEvery { tangemTechApi.activatePromoCode(any()) } returns + ApiResponse.Error( + HttpException(code = HttpException.Code.CONFLICT, message = null, errorBody = null), + ) as ApiResponse + + // WHEN + val result = repository.activatePromoCode(promoCode = "PROMO", bitcoinAddress = "addr") + + // THEN + var error: ActivatePromoCodeError? = null + result.fold({ error = it }, { }) + assertThat(error).isEqualTo(ActivatePromoCodeError.PromocodeAlreadyUsed) + } } \ No newline at end of file diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/PromoCodeActivationResult.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/PromoCodeActivationResult.kt new file mode 100644 index 0000000000..0ca02411b2 --- /dev/null +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/PromoCodeActivationResult.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.wallets + +enum class PromoCodeActivationResult { + Failed, + InvalidPromoCode, + NoBitcoinAddress, + PromoCodeAlreadyUsed, + Activated, +} \ No newline at end of file diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/errors/ActivatePromoCodeError.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/errors/ActivatePromoCodeError.kt new file mode 100644 index 0000000000..cd1accae97 --- /dev/null +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/errors/ActivatePromoCodeError.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.wallets.models.errors + +sealed class ActivatePromoCodeError { + + data object InvalidPromoCode : ActivatePromoCodeError() + + data object ActivationFailed : ActivatePromoCodeError() + + data object PromocodeAlreadyUsed : ActivatePromoCodeError() + + data object NoBitcoinAddress : ActivatePromoCodeError() +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 5a6c051057..2a2f93d29c 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -1,9 +1,11 @@ package com.tangem.domain.wallets.repository +import arrow.core.Either import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.UserWalletRemoteInfo +import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError import kotlinx.coroutines.flow.Flow @Suppress("TooManyFunctions") @@ -58,4 +60,6 @@ interface WalletsRepository { @Throws suspend fun associateWallets(applicationId: String, wallets: List) + + suspend fun activatePromoCode(promoCode: String, bitcoinAddress: String): Either } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ActivateBitcoinPromocodeUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ActivateBitcoinPromocodeUseCase.kt new file mode 100644 index 0000000000..0acd401fcb --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ActivateBitcoinPromocodeUseCase.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError +import com.tangem.domain.wallets.repository.WalletsRepository +import javax.inject.Inject + +class ActivateBitcoinPromocodeUseCase @Inject constructor( + private val walletsRepository: WalletsRepository, +) { + + suspend operator fun invoke(address: String, promoCode: String): Either = + walletsRepository.activatePromoCode(promoCode = promoCode, bitcoinAddress = address) +} \ No newline at end of file diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/deeplink/PromoDeeplinkHandler.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/deeplink/PromoDeeplinkHandler.kt new file mode 100644 index 0000000000..75939e8c55 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/deeplink/PromoDeeplinkHandler.kt @@ -0,0 +1,10 @@ +package com.tangem.features.wallet.deeplink + +import kotlinx.coroutines.CoroutineScope + +interface PromoDeeplinkHandler { + + interface Factory { + fun create(coroutineScope: CoroutineScope, queryParams: Map): PromoDeeplinkHandler + } +} \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 77dd37708d..646bb012f2 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -121,6 +121,8 @@ dependencies { implementation(projects.common.ui) /** Test libraries */ - implementation(deps.test.junit) - implementation(deps.test.truth) + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt new file mode 100644 index 0000000000..bb524e4d59 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt @@ -0,0 +1,142 @@ +package com.tangem.feature.wallet.deeplink + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.routing.deeplink.DeeplinkConst.PROMO_CODE_KEY +import com.tangem.core.decompose.di.GlobalUiMessageSender +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.mask +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.GlobalLoadingMessage +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase +import com.tangem.domain.wallets.PromoCodeActivationResult +import com.tangem.domain.wallets.PromoCodeActivationResult.* +import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError +import com.tangem.domain.wallets.usecase.ActivateBitcoinPromocodeUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.wallet.impl.R +import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber + +internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( + @Assisted private val scope: CoroutineScope, + @Assisted private val queryParams: Map, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, + private val getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase, + private val activateBitcoinPromocodeUseCase: ActivateBitcoinPromocodeUseCase, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, +) : PromoDeeplinkHandler { + + init { + val promoCode = queryParams[PROMO_CODE_KEY].orEmpty() + if (promoCode.isEmpty()) { + showAlert(InvalidPromoCode) + } else { + findSelectedWallet(promoCode) + } + } + + private fun findSelectedWallet(promoCode: String) { + getSelectedWalletSyncUseCase().fold( + ifLeft = { + Timber.tag(LOG_TAG).e("Error on getting user wallet: $it") + showAlert(Failed) + }, + ifRight = { userWallet -> + Timber.tag(LOG_TAG).d("SelectedUserWallet ${userWallet.walletId.stringValue.mask()}") + findBitcoinAddress(userWallet = userWallet, promoCode = promoCode) + }, + ) + } + + private fun findBitcoinAddress(userWallet: UserWallet, promoCode: String) { + scope.launch { + getMultiCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWallet.walletId).onRight { currencies -> + val bitcoinAddress = currencies.find { it.currency.id.rawNetworkId == Blockchain.Bitcoin.id } + ?.value + ?.networkAddress + ?.defaultAddress + ?.value + + if (bitcoinAddress != null) { + Timber.tag(LOG_TAG).d( + "Start activation promoCode ${promoCode.mask()} address ${bitcoinAddress.mask()}", + ) + activatePromoCode(bitcoinAddress = bitcoinAddress, promoCode = promoCode) + } else { + Timber.tag(LOG_TAG).d("no Bitcoin address") + showAlert(NoBitcoinAddress) + } + }.onLeft { + Timber.tag(LOG_TAG).e("Error on getting userWallet currencies: $it") + showAlert(Failed) + } + } + } + + private suspend fun activatePromoCode(bitcoinAddress: String, promoCode: String) { + uiMessageSender.send(GlobalLoadingMessage(true)) + activateBitcoinPromocodeUseCase(bitcoinAddress, promoCode).onRight { + uiMessageSender.send(GlobalLoadingMessage(false)) + delay(DEFAULT_MESSAGE_SENDER_DELAY) + Timber.tag(LOG_TAG).d("${promoCode.mask()} activation success on address ${bitcoinAddress.mask()}") + showAlert(Activated) + }.onLeft { error -> + uiMessageSender.send(GlobalLoadingMessage(false)) + delay(DEFAULT_MESSAGE_SENDER_DELAY) + Timber.tag(LOG_TAG).d("${promoCode.mask()} activation failed $error") + val alertType = when (error) { + ActivatePromoCodeError.ActivationFailed -> Failed + ActivatePromoCodeError.InvalidPromoCode -> InvalidPromoCode + ActivatePromoCodeError.NoBitcoinAddress -> NoBitcoinAddress + ActivatePromoCodeError.PromocodeAlreadyUsed -> PromoCodeAlreadyUsed + } + showAlert(alertType) + } + } + + private fun showAlert(type: PromoCodeActivationResult) { + val (title, message) = when (type) { + Failed -> resourceReference(R.string.bitcoin_promo_activation_error_title) to + resourceReference(R.string.bitcoin_promo_activation_error) + InvalidPromoCode -> resourceReference(R.string.bitcoin_promo_invalid_code_title) to + resourceReference(R.string.bitcoin_promo_invalid_code) + NoBitcoinAddress -> resourceReference(R.string.bitcoin_promo_no_address_title) to + resourceReference(R.string.bitcoin_promo_no_address) + PromoCodeAlreadyUsed -> resourceReference(R.string.bitcoin_promo_already_activated_title) to + resourceReference(R.string.bitcoin_promo_already_activated) + Activated -> resourceReference(R.string.bitcoin_promo_activation_success_title) to + resourceReference(R.string.bitcoin_promo_activation_success) + } + uiMessageSender.send( + DialogMessage( + title = title, + message = message, + dismissOnFirstAction = true, + firstActionBuilder = { + okAction { } + }, + ), + ) + } + + @AssistedFactory + interface Factory : PromoDeeplinkHandler.Factory { + override fun create( + coroutineScope: CoroutineScope, + queryParams: Map, + ): DefaultPromoDeeplinkHandler + } + + companion object { + private const val LOG_TAG = "PromoCodeActivation" + private const val DEFAULT_MESSAGE_SENDER_DELAY = 500L + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/di/WalletDeepLinkModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/di/WalletDeepLinkModule.kt index fca5019c10..f8f1e46718 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/di/WalletDeepLinkModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/di/WalletDeepLinkModule.kt @@ -1,7 +1,9 @@ package com.tangem.feature.wallet.deeplink.di +import com.tangem.feature.wallet.deeplink.DefaultPromoDeeplinkHandler import com.tangem.feature.wallet.deeplink.DefaultWalletDeepLinkActionTrigger import com.tangem.feature.wallet.deeplink.DefaultWalletDeepLinkHandler +import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener import com.tangem.features.wallet.deeplink.WalletDeepLinkActionTrigger import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler @@ -19,6 +21,10 @@ internal interface WalletDeepLinkModule { @Singleton fun bindWalletDeepLinkHandlerFactory(impl: DefaultWalletDeepLinkHandler.Factory): WalletDeepLinkHandler.Factory + @Binds + @Singleton + fun bindPromoDeepLinkHandlerFactory(impl: DefaultPromoDeeplinkHandler.Factory): PromoDeeplinkHandler.Factory + @Binds @Singleton fun bindWalletDeepLinkActionTrigger(impl: DefaultWalletDeepLinkActionTrigger): WalletDeepLinkActionTrigger diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt new file mode 100644 index 0000000000..4733150631 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt @@ -0,0 +1,331 @@ +package com.tangem.feature.wallet.presentation.wallet.deeplink + +import arrow.core.Either +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.routing.deeplink.DeeplinkConst +import com.tangem.core.decompose.ui.UiMessage +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.GetUserWalletError +import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError +import com.tangem.domain.wallets.usecase.ActivateBitcoinPromocodeUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.wallet.deeplink.DefaultPromoDeeplinkHandler +import com.tangem.feature.wallet.impl.R +import io.mockk.CapturingSlot +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.slot +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import timber.log.Timber +import java.math.BigDecimal + +class DefaultPromoDeeplinkHandlerTest { + + @MockK(relaxed = true) + private lateinit var uiMessageSender: UiMessageSender + + @MockK + private lateinit var getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase + + @MockK + private lateinit var activateBitcoinPromocodeUseCase: ActivateBitcoinPromocodeUseCase + + @MockK + private lateinit var getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase + + private lateinit var messageSlot: CapturingSlot + private lateinit var testDispatcher: TestDispatcher + private lateinit var testScope: TestScope + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + + Dispatchers.setMain(testDispatcher) + + MockKAnnotations.init(this) + messageSlot = slot() + every { uiMessageSender.send(capture(messageSlot)) } just runs + + Timber.uprootAll() // Disable Timber logging for tests + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + testScope.cancel() + } + + @Test + fun `GIVEN empty promo code WHEN init THEN invalid promo code dialog is shown`() = runTest { + val queryParams = emptyMap() + + DefaultPromoDeeplinkHandler( + scope = this, + queryParams = queryParams, + uiMessageSender = uiMessageSender, + getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, + activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase, + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + ) + + val sent = messageSlot.captured as DialogMessage + Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_invalid_code_title)) + Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_invalid_code)) + } + + @Test + fun `GIVEN wallet fetch error WHEN getSelectedWalletUseCase THEN failed dialog is shown`() = runTest { + val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to "PROMO123") + every { getSelectedWalletSyncUseCase.invoke() } returns Either.Left(GetUserWalletError.UserWalletNotFound) + + DefaultPromoDeeplinkHandler( + scope = this, + queryParams = queryParams, + uiMessageSender = uiMessageSender, + getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, + activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase, + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + ) + + val sent = messageSlot.captured as DialogMessage + Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error_title)) + Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error)) + } + + @Test + fun `GIVEN token status error WHEN getWalletCurrenciesScreen THEN failed dialog is shown`() = runTest { + val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to "PROMO123") + val userWallet = mockUserWallet("ABCDEF") + every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet) + coEvery { getMultiCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWallet.walletId) } returns Either.Left( + TokenListError.DataError(Exception("boom")), + ) + + DefaultPromoDeeplinkHandler( + scope = this, + queryParams = queryParams, + uiMessageSender = uiMessageSender, + getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, + activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase, + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + ) + + advanceUntilIdle() + + val sent = messageSlot.captured as DialogMessage + Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error_title)) + Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error)) + } + + @Test + fun `GIVEN no bitcoin address WHEN getWalletCurrenciesScreen THEN no bitcoin address dialog is shown`() = runTest { + val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to "PROMO123") + val userWallet = mockUserWallet("ABCDEF") + every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet) + // Return only ETH status so BTC is not found + val ethStatus = buildStatusForNetwork(rawNetworkId = "ethereum", address = "0x123") + coEvery { getMultiCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWallet.walletId) } returns Either.Right( + listOf(ethStatus), + ) + + DefaultPromoDeeplinkHandler( + scope = this, + queryParams = queryParams, + uiMessageSender = uiMessageSender, + getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, + activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase, + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + ) + + advanceUntilIdle() + + val sent = messageSlot.captured as DialogMessage + Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title)) + Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address)) + } + + @Test + fun `GIVEN activation success WHEN activatePromoCode THEN activated dialog is shown`() = runTest { + val promoCode = "PROMO123" + val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode) + val userWallet = mockUserWallet("ABCDEF") + every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet) + val btcStatus = buildStatusForNetwork(rawNetworkId = Blockchain.Bitcoin.id, address = "bc1qxyz") + coEvery { getMultiCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWallet.walletId) } returns Either.Right( + listOf(btcStatus), + ) + coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qxyz", promoCode) } returns Either.Right("ok") + + DefaultPromoDeeplinkHandler( + scope = this, + queryParams = queryParams, + uiMessageSender = uiMessageSender, + getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, + activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase, + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + ) + + advanceUntilIdle() + + val sent = messageSlot.captured as DialogMessage + Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success_title)) + Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success)) + } + + @Test + fun `GIVEN activation invalid code WHEN activatePromoCode THEN invalid promo code dialog is shown`() = runTest { + runActivationErrorCase( + error = ActivatePromoCodeError.InvalidPromoCode, + expectedTitle = resourceReference(R.string.bitcoin_promo_invalid_code_title), + expectedMessage = resourceReference(R.string.bitcoin_promo_invalid_code), + ) + } + + @Test + fun `GIVEN activation failed WHEN activatePromoCode THEN failed dialog is shown`() = runTest { + runActivationErrorCase( + error = ActivatePromoCodeError.ActivationFailed, + expectedTitle = resourceReference(R.string.bitcoin_promo_activation_error_title), + expectedMessage = resourceReference(R.string.bitcoin_promo_activation_error), + ) + } + + @Test + fun `GIVEN activation no address WHEN activatePromoCode THEN no bitcoin address dialog is shown`() = runTest { + runActivationErrorCase( + error = ActivatePromoCodeError.NoBitcoinAddress, + expectedTitle = resourceReference(R.string.bitcoin_promo_no_address_title), + expectedMessage = resourceReference(R.string.bitcoin_promo_no_address), + ) + } + + @Test + fun `GIVEN activation already used WHEN activatePromoCode THEN already activated dialog is shown`() = runTest { + runActivationErrorCase( + error = ActivatePromoCodeError.PromocodeAlreadyUsed, + expectedTitle = resourceReference(R.string.bitcoin_promo_already_activated_title), + expectedMessage = resourceReference(R.string.bitcoin_promo_already_activated), + ) + } + + private fun runActivationErrorCase( + error: ActivatePromoCodeError, + expectedTitle: TextReference, + expectedMessage: TextReference, + ) = runTest { + val promoCode = "PROMO123" + val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode) + val userWallet = mockUserWallet("ABCDEF") + every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet) + val btcStatus = buildStatusForNetwork(rawNetworkId = Blockchain.Bitcoin.id, address = "bc1qxyz") + coEvery { getMultiCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWallet.walletId) } returns Either.Right( + listOf(btcStatus), + ) + coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qxyz", promoCode) } returns Either.Left(error) + + DefaultPromoDeeplinkHandler( + scope = this, + queryParams = queryParams, + uiMessageSender = uiMessageSender, + getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, + activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase, + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + ) + + advanceUntilIdle() + + val sent = messageSlot.captured as DialogMessage + Truth.assertThat(sent.title).isEqualTo(expectedTitle) + Truth.assertThat(sent.message).isEqualTo(expectedMessage) + } + + private fun mockUserWallet(id: String): UserWallet { + val userWallet = mockk(relaxed = true) + every { userWallet.walletId } returns UserWalletId(id) + return userWallet + } + + private fun buildStatusForNetwork(rawNetworkId: String, address: String): CryptoCurrencyStatus { + val networkId = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None) + val network = Network( + id = networkId, + backendId = rawNetworkId, + name = rawNetworkId, + currencySymbol = rawNetworkId.take(3).uppercase(), + derivationPath = Network.DerivationPath.None, + isTestnet = false, + standardType = Network.StandardType.Unspecified("UNSPECIFIED"), + hasFiatFeeRate = false, + canHandleTokens = false, + transactionExtrasType = Network.TransactionExtrasType.NONE, + ) + + val currencyId = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId), + suffix = CryptoCurrency.ID.Suffix.RawID(rawNetworkId), + ) + + val coin = CryptoCurrency.Coin( + id = currencyId, + network = network, + name = rawNetworkId, + symbol = network.currencySymbol, + decimals = 8, + iconUrl = null, + isCustom = false, + ) + + val value = CryptoCurrencyStatus.NoQuote( + amount = BigDecimal.ZERO, + yieldBalance = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = address, + type = NetworkAddress.Address.Type.Primary, + ), + ), + sources = CryptoCurrencyStatus.Sources(), + ) + + return CryptoCurrencyStatus( + currency = coin, + value = value, + ) + } +} \ No newline at end of file From ab72c61f5f184730b6f017e90f10c501a1bee391 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Aug 2025 15:30:37 +0500 Subject: [PATCH 03/10] Updated on 2026-08-14 --- .../deeplink/DefaultPromoDeeplinkHandler.kt | 6 ++++ .../analytics/PromoActivationAnalytics.kt | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/analytics/PromoActivationAnalytics.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt index bb524e4d59..c927aa7000 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.deeplink import com.tangem.blockchain.common.Blockchain import com.tangem.common.routing.deeplink.DeeplinkConst.PROMO_CODE_KEY +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.mask @@ -15,6 +16,7 @@ import com.tangem.domain.wallets.PromoCodeActivationResult.* import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError import com.tangem.domain.wallets.usecase.ActivateBitcoinPromocodeUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.wallet.deeplink.analytics.PromoActivationAnalytics import com.tangem.feature.wallet.impl.R import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler import dagger.assisted.Assisted @@ -25,6 +27,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber +@Suppress("LongParameterList") internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( @Assisted private val scope: CoroutineScope, @Assisted private val queryParams: Map, @@ -32,9 +35,11 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( private val getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase, private val activateBitcoinPromocodeUseCase: ActivateBitcoinPromocodeUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val analyticsEventsHandler: AnalyticsEventHandler, ) : PromoDeeplinkHandler { init { + analyticsEventsHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) val promoCode = queryParams[PROMO_CODE_KEY].orEmpty() if (promoCode.isEmpty()) { showAlert(InvalidPromoCode) @@ -103,6 +108,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( } private fun showAlert(type: PromoCodeActivationResult) { + analyticsEventsHandler.send(PromoActivationAnalytics.PromoActivation(type)) val (title, message) = when (type) { Failed -> resourceReference(R.string.bitcoin_promo_activation_error_title) to resourceReference(R.string.bitcoin_promo_activation_error) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/analytics/PromoActivationAnalytics.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/analytics/PromoActivationAnalytics.kt new file mode 100644 index 0000000000..9bcd32f457 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/analytics/PromoActivationAnalytics.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.wallet.deeplink.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.wallets.PromoCodeActivationResult + +sealed class PromoActivationAnalytics( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = "Promotion", event = event, params = params) { + + data object PromoDeepLinkActivationStart : PromoActivationAnalytics( + event = "Bitcoin Promo Deep Link Activation", + params = emptyMap(), + ) + + data class PromoActivation(val result: PromoCodeActivationResult) : PromoActivationAnalytics( + event = "Bitcoin Promo Activation", + params = mapOf( + "State" to when (result) { + PromoCodeActivationResult.Failed -> "Error" + PromoCodeActivationResult.InvalidPromoCode -> "Invalid" + PromoCodeActivationResult.NoBitcoinAddress -> "No Address " + PromoCodeActivationResult.PromoCodeAlreadyUsed -> "Already Use" + PromoCodeActivationResult.Activated -> "Activated" + }, + ), + ) +} \ No newline at end of file From b152c36c19e7ace4fdf33454ab5d6e5919bda09a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Aug 2025 16:05:23 +0500 Subject: [PATCH 04/10] Updated on 2026-08-14 --- .../DefaultPromoDeeplinkHandlerTest.kt | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt index 4733150631..e355a21829 100644 --- a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt @@ -4,6 +4,7 @@ import arrow.core.Either import com.google.common.truth.Truth import com.tangem.blockchain.common.Blockchain import com.tangem.common.routing.deeplink.DeeplinkConst +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.ui.UiMessage import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.TextReference @@ -22,6 +23,7 @@ import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError import com.tangem.domain.wallets.usecase.ActivateBitcoinPromocodeUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.wallet.deeplink.DefaultPromoDeeplinkHandler +import com.tangem.feature.wallet.deeplink.analytics.PromoActivationAnalytics import com.tangem.feature.wallet.impl.R import io.mockk.CapturingSlot import io.mockk.MockKAnnotations @@ -32,6 +34,7 @@ import io.mockk.just import io.mockk.mockk import io.mockk.runs import io.mockk.slot +import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel @@ -47,6 +50,7 @@ import org.junit.Before import org.junit.Test import timber.log.Timber import java.math.BigDecimal +import com.tangem.domain.wallets.PromoCodeActivationResult class DefaultPromoDeeplinkHandlerTest { @@ -62,6 +66,9 @@ class DefaultPromoDeeplinkHandlerTest { @MockK private lateinit var getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase + @MockK + private lateinit var analyticsEventHandler: AnalyticsEventHandler + private lateinit var messageSlot: CapturingSlot private lateinit var testDispatcher: TestDispatcher private lateinit var testScope: TestScope @@ -75,6 +82,7 @@ class DefaultPromoDeeplinkHandlerTest { Dispatchers.setMain(testDispatcher) MockKAnnotations.init(this) + every { analyticsEventHandler.send(any()) } returns Unit messageSlot = slot() every { uiMessageSender.send(capture(messageSlot)) } just runs @@ -99,11 +107,19 @@ class DefaultPromoDeeplinkHandlerTest { getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + analyticsEventsHandler = analyticsEventHandler, ) val sent = messageSlot.captured as DialogMessage Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_invalid_code_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_invalid_code)) + + verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify(exactly = 1) { + analyticsEventHandler.send( + PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.InvalidPromoCode), + ) + } } @Test @@ -118,11 +134,19 @@ class DefaultPromoDeeplinkHandlerTest { getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + analyticsEventsHandler = analyticsEventHandler, ) val sent = messageSlot.captured as DialogMessage Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error)) + + verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify(exactly = 1) { + analyticsEventHandler.send( + PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Failed), + ) + } } @Test @@ -141,6 +165,7 @@ class DefaultPromoDeeplinkHandlerTest { getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + analyticsEventsHandler = analyticsEventHandler, ) advanceUntilIdle() @@ -148,6 +173,13 @@ class DefaultPromoDeeplinkHandlerTest { val sent = messageSlot.captured as DialogMessage Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error)) + + verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify(exactly = 1) { + analyticsEventHandler.send( + PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Failed), + ) + } } @Test @@ -168,6 +200,7 @@ class DefaultPromoDeeplinkHandlerTest { getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + analyticsEventsHandler = analyticsEventHandler, ) advanceUntilIdle() @@ -175,6 +208,13 @@ class DefaultPromoDeeplinkHandlerTest { val sent = messageSlot.captured as DialogMessage Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address)) + + verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify(exactly = 1) { + analyticsEventHandler.send( + PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), + ) + } } @Test @@ -196,6 +236,7 @@ class DefaultPromoDeeplinkHandlerTest { getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + analyticsEventsHandler = analyticsEventHandler, ) advanceUntilIdle() @@ -203,6 +244,13 @@ class DefaultPromoDeeplinkHandlerTest { val sent = messageSlot.captured as DialogMessage Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success)) + + verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify(exactly = 1) { + analyticsEventHandler.send( + PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated), + ) + } } @Test @@ -212,6 +260,13 @@ class DefaultPromoDeeplinkHandlerTest { expectedTitle = resourceReference(R.string.bitcoin_promo_invalid_code_title), expectedMessage = resourceReference(R.string.bitcoin_promo_invalid_code), ) + + verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify(exactly = 1) { + analyticsEventHandler.send( + PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.InvalidPromoCode), + ) + } } @Test @@ -221,6 +276,13 @@ class DefaultPromoDeeplinkHandlerTest { expectedTitle = resourceReference(R.string.bitcoin_promo_activation_error_title), expectedMessage = resourceReference(R.string.bitcoin_promo_activation_error), ) + + verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify(exactly = 1) { + analyticsEventHandler.send( + PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Failed), + ) + } } @Test @@ -230,6 +292,13 @@ class DefaultPromoDeeplinkHandlerTest { expectedTitle = resourceReference(R.string.bitcoin_promo_no_address_title), expectedMessage = resourceReference(R.string.bitcoin_promo_no_address), ) + + verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify(exactly = 1) { + analyticsEventHandler.send( + PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), + ) + } } @Test @@ -239,6 +308,13 @@ class DefaultPromoDeeplinkHandlerTest { expectedTitle = resourceReference(R.string.bitcoin_promo_already_activated_title), expectedMessage = resourceReference(R.string.bitcoin_promo_already_activated), ) + + verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify(exactly = 1) { + analyticsEventHandler.send( + PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.PromoCodeAlreadyUsed), + ) + } } private fun runActivationErrorCase( @@ -263,6 +339,7 @@ class DefaultPromoDeeplinkHandlerTest { getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + analyticsEventsHandler = analyticsEventHandler, ) advanceUntilIdle() From b7bea8357df1e7524619f48a922d0fe88ad7a8fd Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 10:16:07 +0500 Subject: [PATCH 05/10] Updated on 2026-08-14 --- core/res/src/main/res/values-fr/strings.xml | 10 +++++ core/res/src/main/res/values-ja/strings.xml | 43 ++++++++++++++++++--- core/res/src/main/res/values/strings.xml | 4 +- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index e907d8df01..97e1096155 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -60,6 +60,16 @@ Veuillez réessayer dans 30 secondes ou scannez la carte/bague Trop de tentatives Vous avez désactivé l\'authentification biométrique sur votre téléphone et ne pourrez pas enregistrer de portefeuilles dans l\'application. Pour enregistrer des portefeuilles, veuillez activer la fonction d\'authentification biométrique dans les paramètres de votre téléphone. + Une erreur s\'est produite lors du traitement de votre code promo. Veuillez réessayer plus tard. + Erreur d\'activation + Votre code promo a été activé avec succès. Un bonus de 10 USDT en Bitcoin sera crédité dans 14 jours. + Code promo activé + Ce code promo a déjà été utilisé et ne peut pas être activé à nouveau. + Code indisponible + Ce code promo n\'est pas valide et ne peut pas être activé. + Code invalide + Une adresse Bitcoin est nécessaire pour recevoir le bonus. Veuillez en ajouter une à votre portefeuille et réessayer l\'activation. + Adresse Bitcoin requise Démarrer le processus de sauvegarde Utilisez une carte bancaire ou d\'autres moyens de paiement diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 4c5d34d274..0b8555b8da 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -12,8 +12,12 @@ ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 アクセスコードの作成 アクセスコード + %1$s個を超えるアカウントは作成できません。新しいアカウントを追加するには、1つをアーカイブしてください。 + 新しいアカウントを追加できません アーカイブされたアカウント 回復する + 「 %1$s 」を回復しようとしています。 + アカウントを回復する アーカイブ済み アカウントをアーカイブする アーカイブ @@ -27,6 +31,7 @@ 新しいアカウント アカウントを追加 アカウントを編集 + %1$s ( %2$s内) アカウントを長押しして並べ替える 編集を続ける 破棄 @@ -73,8 +78,10 @@ 設定に移動して、Tangemアプリで生体認証を有効にします。 生体認証を有効にする %1$sが無効になると、アプリのロックを解除してウォレットを操作するために、パスコードを入力する必要があります。 + 後でウォレットのアクセスコードを入力してもらいます。それを安全に保存し、今後の利用に備えるためです。 これにより、保存されているウォレットアクセスコードがすべて削除されます。ウォレットでの今後の操作には、アクセスコードの送信が必要になります。 保存したデバイスを削除すると、保存されているすべてのウォレットとそのアクセスコードがアプリから削除されます。 + これにより、保存されているウォレットアクセスコードがすべて削除されます。今後ウォレットを操作するには、アクセスコードの送信が必要になります。 アクセスコードを要求する このオプションを選択すると、機密性の高い操作における生体認証が無効になります。取引の署名時などには、毎回アクセスコードの入力が必要になります。 アクセスコードを保存 @@ -117,11 +124,15 @@ 試行回数が多すぎます お使いの携帯電話で生体認証が無効になっているため、アプリにウォレットを保存できません。ウォレットを保存するには、携帯電話の設定で生体認証機能を有効にしてください。 プロモーションコードの処理中にエラーが発生しました。しばらくしてからもう一度お試しください。 + アクティベーションエラー プロモーションコードが正常に有効化されました。14日以内に10 USDT相当のビットコインボーナスが付与されます。 プロモーションコードが有効になりました このプロモーションコードは既に使用されているため、再度使うことはできません。 + コードが利用できません このプロモーションコードは無効であり、有効化できません。 + 無効なコード ボーナスを受け取るにはビットコインアドレスが必要です。ウォレットにビットコインアドレスを追加し、再度アクティベーションをお試しください。 + ビットコインアドレスが必要です バックアップ処理を開始する 銀行カードまたはその他の支払い方法を使用する @@ -241,6 +252,9 @@ ネットワーク手数料 送金額は、選択された手数料レベルをカバーするため、%1$s (%2$s) 減額されます。 + + %dネットワーク + NFT いいえ @@ -367,8 +381,8 @@ 他のネットワークで資産を送金すると、永久に失われます。 %sネットワーク 下記のみを使用して資金を送金する - サポートチームの皆様、コード %s のエラーが発生しました。 - WalletConnect エラー + こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。 + WalletConnectエラー 別のウォレットのカードまたはリングを使用しました。このウォレットにリンクしているカードまたはリングをタップしてください。 取引に必要な資金が不足しています。アカウントに入金してください。 マイトークン @@ -485,6 +499,8 @@ 実行すると、最初からやり直す必要があります。 Googleドライブのバックアップに保存されている既存のウォレットを復元する Googleドライブのバックアップ + Tangemの業界最高水準のハードウェアウォレットで、今すぐセキュリティをアップグレードしましょう。 + ハードウェアウォレット バックアップへ移動 アクセスコードを使用してウォレットを保護するには、まずバックアップを完了してください。 まずバックアップを完了する @@ -496,7 +512,21 @@ 最新の機能とニュースをお届けします シードフレーズのバックアップ モバイルウォレットを作成する + このリカバリーフレーズはすでにインポートされています。 モバイルウォレット + 手続き中も資金は安全に保管され、完全にアクセス可能です + 資金へのアクセス + すべてのプライベートウォレットデータはモバイルアプリから削除され、Tangemデバイスにのみ安全に保存されます。 + セキュリティ全般 + 秘密鍵は、アプリからTangemカード・リングに移動します + 鍵の移行 + デバイスをスキャン + アップグレードを開始 + ウォレットをTangemウォレットにアップグレードします。これにより、コールドストレージで資産を安全に保管できます。 + Tangemウォレット + ハードウェアウォレットにアップグレード + Tangemの業界最高水準のハードウェアウォレットで、暗号資産を安全に保管しましょう。 + ハードウェアバックアップでウォレットをアップグレード この情報はAIで生成されました。 \nエラーが見つかった場合は、ここをタップしてください。 アクセスコードを変更するには、上図のようにカードまたはリングをタップし、操作が終了するまで取り外さないでください。 パスコードを変更するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。 @@ -696,6 +726,9 @@ 今すぐ参加 コードを共有すると、販売ごとに5 USDTを獲得できます。お友達は10%割引になります。 友達への紹介で報酬を獲得しよう! + 暗号資産を買い付ける + SEPA送金で暗号資産を買い付けると、手数料はかかりません。 + SEPAで暗号資産を買い付ける すべてのデバイスを保護するには、単一のアクセスコードを設定してください。 保護する 後で各カードおよびリングに個別のアクセスコードを設定できます。 @@ -1287,10 +1320,10 @@ Dapp%1$s 、BNB取引の署名を要求しています\n\n%2$s %1$sの取引注文\n価格: %2$s\n受取金額: %3$s\n支払金額: %4$s 取引の詳細:\n送信元: %1$s\n受取先: %2$s\n量: %3$s - クリップボードには WalletConnect コードが含まれています。コピーした値を使用するか、QRコードをスキャンしてください。 + クリップボードには WalletConnectコードが含まれています。コピーした値を使用するか、QRコードをスキャンしてください。 %1$sの取引を作成するリクエスト\n%2$s\n\n金額: %3$s\n手数料: %4$s\n合計: %5$s\n残高: %6$s 取引を送信できません。資金が足りません。 - WalletConnect セッションを確立できませんでした。しばらくしてからもう一度お試しください。 + WalletConnectセッションを確立できませんでした。しばらくしてからもう一度お試しください。 すべてのトークンがリストには追加されませんでした。まず追加してから、もう一度お試しください。不足しているトークン: \n メッセージの署名に失敗しました。\nもう一度お試しください。 WalletConnectセッションの確立に失敗しました:タイムアウトエラー。しばらくしてもう一度お試しください。 @@ -1469,7 +1502,7 @@ サポートされていないdApp エラーコード: 8 005。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 不明なエラーが発生しました - このネットワーク %s はTangem Walletでサポートされておらず、接続できません。 + このネットワーク%sはTangemウォレットでサポートされておらず、接続できません。 サポートされていないネットワーク Tangemは現在%sで必要なネットワークをサポートしていません。 未対応のネットワーク diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8387e25d6a..c69bcd90ed 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -126,8 +126,8 @@ You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. An error occurred while processing your promo code. Please try again later. Activation error - Your promo code has been successfully activated. A bonus of 10 USDT in Bitcoin will be credited in 14 days. - Promo code activated + Your promo code was successfully activated. A bonus of 10 USDT in Bitcoin will be credited to your account within 14 days. + Promo Code Activated This promo code has already been used and cannot be activated again. Code unavailable This promo code is not valid and cannot be activated. From a378cb57478f40fc013a2b529ad8f60ccd5a0df6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Aug 2025 15:26:54 +0500 Subject: [PATCH 06/10] Updated on 2026-08-14 --- .../send/WcSendTransactionComponent.kt | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt index 62f838c9b7..5e78c040a8 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt @@ -19,27 +19,13 @@ internal class WcSendTransactionComponent( private val feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, ) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { - private val feeSelectorBlockComponent by lazy { - val state = requireNotNull(model.uiState.value) { "in this step state should be not null" } - feeSelectorBlockComponentFactory.create( - context = appComponentContext, - params = FeeSelectorParams.FeeSelectorBlockParams( - state = state.feeSelectorUM, - onLoadFee = model::loadFee, - cryptoCurrencyStatus = model.cryptoCurrencyStatus, - feeCryptoCurrencyStatus = model.cryptoCurrencyStatus, - feeStateConfiguration = model.feeStateConfiguration, - feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet, - ), - onResult = model::updateFee, - ) - } + private var feeSelectorBlockComponent: FeeSelectorBlockComponent? = null init { lifecycle.doOnResume { val state = model.uiState.value if (state?.transaction?.feeState is WcTransactionFeeState.Success) { - feeSelectorBlockComponent.updateState(state.feeSelectorUM) + feeSelectorBlockComponent?.updateState(state.feeSelectorUM) } } } @@ -54,8 +40,12 @@ internal class WcSendTransactionComponent( val state = content?.transaction if (state != null) { - val feeSelectorBlock = - if (state.feeState !is WcTransactionFeeState.None) feeSelectorBlockComponent else null + val feeSelectorUM = content?.feeSelectorUM + val feeSelectorBlock = if (state.feeState !is WcTransactionFeeState.None && feeSelectorUM != null) { + getFeeSelectorBlockComponent(feeSelectorUM) + } else { + null + } WcSendTransactionModalBottomSheet( state = state, feeSelectorBlockComponent = feeSelectorBlock, @@ -67,4 +57,26 @@ internal class WcSendTransactionComponent( ) } } + + private fun getFeeSelectorBlockComponent(feeSelectorUM: FeeSelectorUM): FeeSelectorBlockComponent { + val local = feeSelectorBlockComponent + return if (local != null) { + local + } else { + val component = feeSelectorBlockComponentFactory.create( + context = appComponentContext, + params = FeeSelectorParams.FeeSelectorBlockParams( + state = feeSelectorUM, + onLoadFee = model::loadFee, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = model.cryptoCurrencyStatus, + feeStateConfiguration = model.feeStateConfiguration, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet, + ), + onResult = model::updateFee, + ) + feeSelectorBlockComponent = component + component + } + } } \ No newline at end of file From 8c926b56262056724bb7ed870c484215b9ba63ce Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 21:50:18 +0700 Subject: [PATCH 07/10] Updated on 2026-08-14 --- .../domain/walletconnect/model/WcEthMethod.kt | 2 +- .../model/WcEthSignTypedDataParams.kt | 58 +------------------ 2 files changed, 3 insertions(+), 57 deletions(-) diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt index 34b43e11bb..0157a3dc6c 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt @@ -13,7 +13,7 @@ sealed interface WcEthMethod : WcMethod { val account: String, val dataForSign: String, ) : WcEthMethod { - val humanMsg: String = params.message?.contents.orEmpty() + val humanMsg: String = params.message.orEmpty() } data class SendTransaction( diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthSignTypedDataParams.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthSignTypedDataParams.kt index b14e6af6df..60ff88d729 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthSignTypedDataParams.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthSignTypedDataParams.kt @@ -5,60 +5,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class WcEthSignTypedDataParams( - @Json(name = "domain") - val domain: Domain?, @Json(name = "message") - val message: Message?, - @Json(name = "primaryType") - val primaryType: String?, - @Json(name = "types") - val types: Map>, -) { - @JsonClass(generateAdapter = true) - data class Domain( - @Json(name = "chainId") - val chainId: Int?, - @Json(name = "name") - val name: String?, - @Json(name = "verifyingContract") - val verifyingContract: String?, - @Json(name = "version") - val version: String?, - ) - - @JsonClass(generateAdapter = true) - data class Message( - @Json(name = "contents") - val contents: String?, - @Json(name = "from") - val from: Address?, - @Json(name = "to") - val to: Address?, - ) { - @JsonClass(generateAdapter = true) - data class Address( - @Json(name = "name") - val name: String, - @Json(name = "wallet") - val wallet: String, - ) - } - - @JsonClass(generateAdapter = true) - data class Types( - @Json(name = "EIP712Domain") - val eIP712Domain: List = listOf(), - @Json(name = "Mail") - val mail: List = listOf(), - @Json(name = "Person") - val person: List = listOf(), - ) { - @JsonClass(generateAdapter = true) - data class Type( - @Json(name = "name") - val name: String, - @Json(name = "type") - val type: String, - ) - } -} \ No newline at end of file + val message: String?, +) \ No newline at end of file From f2e2bfe1875e4fc9a7aaed2744839ab96c83133c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 20:40:42 +0300 Subject: [PATCH 08/10] 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 3ac868e93f..d3ba082fad 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 3ac868e93f88498258867d457f8b8c4577b40f98 +Subproject commit d3ba082fadbdb583a45cc1c714c57b382cbbe6ef From f1085eea2f65963a0d85a693cc94aeb429aa3610 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 20:42:33 +0300 Subject: [PATCH 09/10] 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 d3ba082fad..7d225a195e 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit d3ba082fadbdb583a45cc1c714c57b382cbbe6ef +Subproject commit 7d225a195eb001f9f4ce88aa4a6fa2d965b9159c From df5af9afab63c68a435bc739e5d5964db317a57c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Aug 2025 12:15:10 +0000 Subject: [PATCH 10/10] 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 57e50cf4ac..c57f7b4239 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.27.0-1148" +tangemBlockchainSdk = "develop-1205" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.27.0-519" +tangemCardSdk = "develop-557" #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-454" +tangemHotSdk = "develop-525" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^