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 51fd1ce19a..be6fe202e7 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
@@ -141,6 +141,11 @@ interface TangemTechApi {
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
+
// region account
@GET("/v1/wallets/{walletId}/accounts")
suspend fun getWalletAccounts(@Path("walletId") walletId: String): ApiResponse
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-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 43ea6c53bf..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内)
アカウントを長押しして並べ替える
編集を続ける
破棄
@@ -119,11 +124,15 @@
試行回数が多すぎます
お使いの携帯電話で生体認証が無効になっているため、アプリにウォレットを保存できません。ウォレットを保存するには、携帯電話の設定で生体認証機能を有効にしてください。
プロモーションコードの処理中にエラーが発生しました。しばらくしてからもう一度お試しください。
+ アクティベーションエラー
プロモーションコードが正常に有効化されました。14日以内に10 USDT相当のビットコインボーナスが付与されます。
プロモーションコードが有効になりました
このプロモーションコードは既に使用されているため、再度使うことはできません。
+ コードが利用できません
このプロモーションコードは無効であり、有効化できません。
+ 無効なコード
ボーナスを受け取るにはビットコインアドレスが必要です。ウォレットにビットコインアドレスを追加し、再度アクティベーションをお試しください。
+ ビットコインアドレスが必要です
バックアップ処理を開始する
銀行カードまたはその他の支払い方法を使用する
@@ -243,6 +252,9 @@
月
ネットワーク手数料
送金額は、選択された手数料レベルをカバーするため、%1$s (%2$s) 減額されます。
+
+ - %dネットワーク
+
次
NFT
いいえ
@@ -369,8 +381,8 @@
他のネットワークで資産を送金すると、永久に失われます。
%sネットワーク
下記のみを使用して資金を送金する
- サポートチームの皆様、コード %s のエラーが発生しました。
- WalletConnect エラー
+ こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。
+ WalletConnectエラー
別のウォレットのカードまたはリングを使用しました。このウォレットにリンクしているカードまたはリングをタップしてください。
取引に必要な資金が不足しています。アカウントに入金してください。
マイトークン
@@ -714,6 +726,9 @@
今すぐ参加
コードを共有すると、販売ごとに5 USDTを獲得できます。お友達は10%割引になります。
友達への紹介で報酬を獲得しよう!
+ 暗号資産を買い付ける
+ SEPA送金で暗号資産を買い付けると、手数料はかかりません。
+ SEPAで暗号資産を買い付ける
すべてのデバイスを保護するには、単一のアクセスコードを設定してください。
保護する
後で各カードおよびリングに個別のアクセスコードを設定できます。
@@ -1305,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セッションの確立に失敗しました:タイムアウトエラー。しばらくしてもう一度お試しください。
@@ -1487,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 0db4cc4be9..c69bcd90ed 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -125,11 +125,15 @@
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.
- Your promo code has been successfully activated. A bonus of 10 USDT in Bitcoin will be credited in 14 days.
- Promo code activated
+ Activation error
+ 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.
+ 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
@@ -735,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 d2a4f5cea1..d8edb5dc18 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
@@ -24,6 +29,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
@@ -35,7 +41,7 @@ import kotlin.collections.mutableSetOf
typealias SeedPhraseNotificationsStatuses = Map
-@Suppress("TooManyFunctions")
+@Suppress("TooManyFunctions", "LargeClass")
internal class DefaultWalletsRepository(
private val appPreferencesStore: AppPreferencesStore,
private val tangemTechApi: TangemTechApi,
@@ -387,4 +393,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 447c2effdf..f305beb722 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/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
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 b65e7006cc..7513e6e3bf 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")
@@ -72,4 +74,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 a8fffc2ea0..1df9de0f95 100644
--- a/features/wallet/impl/build.gradle.kts
+++ b/features/wallet/impl/build.gradle.kts
@@ -123,6 +123,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..c927aa7000
--- /dev/null
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt
@@ -0,0 +1,148 @@
+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
+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.deeplink.analytics.PromoActivationAnalytics
+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
+
+@Suppress("LongParameterList")
+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,
+ private val analyticsEventsHandler: AnalyticsEventHandler,
+) : PromoDeeplinkHandler {
+
+ init {
+ analyticsEventsHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart)
+ 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) {
+ 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)
+ 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/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
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..e9ccd6865a
--- /dev/null
+++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt
@@ -0,0 +1,395 @@
+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.analytics.api.AnalyticsEventHandler
+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.currency.CryptoCurrencyStatus
+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.wallets.PromoCodeActivationResult
+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.deeplink.analytics.PromoActivationAnalytics
+import com.tangem.feature.wallet.impl.R
+import io.mockk.*
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.test.*
+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
+
+ @MockK
+ private lateinit var analyticsEventHandler: AnalyticsEventHandler
+
+ 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)
+ every { analyticsEventHandler.send(any()) } returns Unit
+ 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,
+ 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
+ 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,
+ 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
+ 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,
+ analyticsEventsHandler = analyticsEventHandler,
+ )
+
+ 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))
+
+ verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
+ verify(exactly = 1) {
+ analyticsEventHandler.send(
+ PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Failed),
+ )
+ }
+ }
+
+ @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,
+ analyticsEventsHandler = analyticsEventHandler,
+ )
+
+ 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))
+
+ verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
+ verify(exactly = 1) {
+ analyticsEventHandler.send(
+ PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
+ )
+ }
+ }
+
+ @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,
+ analyticsEventsHandler = analyticsEventHandler,
+ )
+
+ 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))
+
+ verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
+ verify(exactly = 1) {
+ analyticsEventHandler.send(
+ PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated),
+ )
+ }
+ }
+
+ @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),
+ )
+
+ verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
+ verify(exactly = 1) {
+ analyticsEventHandler.send(
+ PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.InvalidPromoCode),
+ )
+ }
+ }
+
+ @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),
+ )
+
+ verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
+ verify(exactly = 1) {
+ analyticsEventHandler.send(
+ PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Failed),
+ )
+ }
+ }
+
+ @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),
+ )
+
+ verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
+ verify(exactly = 1) {
+ analyticsEventHandler.send(
+ PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
+ )
+ }
+ }
+
+ @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),
+ )
+
+ verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
+ verify(exactly = 1) {
+ analyticsEventHandler.send(
+ PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.PromoCodeAlreadyUsed),
+ )
+ }
+ }
+
+ 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,
+ analyticsEventsHandler = analyticsEventHandler,
+ )
+
+ 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,
+ nameResolvingType = Network.NameResolvingType.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
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
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 a974a6874f..c5b943b9b4 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
@@ -20,28 +20,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,
- analyticsCategoryName = WcAnalyticEvents.WC_CATEGORY_NAME,
- ),
- 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)
}
}
}
@@ -56,8 +41,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,
@@ -69,4 +58,27 @@ 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,
+ analyticsCategoryName = WcAnalyticEvents.WC_CATEGORY_NAME,
+ ),
+ onResult = model::updateFee,
+ )
+ feeSelectorBlockComponent = component
+ component
+ }
+ }
}
\ No newline at end of file
diff --git a/tangem-android-tools b/tangem-android-tools
index 428b83bb37..794a8187e6 160000
--- a/tangem-android-tools
+++ b/tangem-android-tools
@@ -1 +1 @@
-Subproject commit 428b83bb378b615209e23afa05c88c454d06a9f1
+Subproject commit 794a8187e6d248ca3c21661df199a34ffeb0037a