diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml
index 37b3938536..7abdfe3162 100644
--- a/core/res/src/main/res/values-es/strings.xml
+++ b/core/res/src/main/res/values-es/strings.xml
@@ -78,6 +78,16 @@
Por favor, inténtelo de nuevo en 30 segundos o escanee la tarjeta/anillo
Demasiados intentos
Ha desactivado la autenticación biométrica en su teléfono y no podrá guardar billeteras en la aplicación. Para guardar billeteras, active la función de autenticación biométrica en los ajustes de su teléfono.
+ Se produjo un error al procesar tu código promocional. Inténtalo de nuevo más tarde.
+ Error de activación
+ Su código promocional se ha activado correctamente. Un bono de 10 USDT en Bitcoin se abonará en su cuenta en un plazo de 14 días.
+ Código promocional activado
+ Este código promocional ya ha sido utilizado y no puede activarse de nuevo.
+ Código no disponible
+ Este código promocional no es válido y no puede activarse.
+ Código inválido
+ Se requiere una dirección de Bitcoin para recibir el bono. Añade una a tu billetera y vuelve a intentar la activación.
+ Se requiere una dirección de Bitcoin
Iniciar proceso de backup
Utilice una tarjeta bancaria u otros métodos de pago
diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml
index 899b661b01..b94dfccc7f 100644
--- a/core/res/src/main/res/values-ru/strings.xml
+++ b/core/res/src/main/res/values-ru/strings.xml
@@ -922,7 +922,7 @@
Обмен и отправка
Продолжить с конвертацией? Это действие удалит предыдущие данные
Подтвердить конвертацию
- Отправьте любой токен, и мы конвертируем его по пути. Адресат получит именно то, что нужно — без лишних действий.
+ Выберите любой токен к получению. Ваш адресат получит ровно то, что вы выбрали — без лишних сложностей.
Будет получено
Получателю
Сумма к получению
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 21880f895f..1175b3d951 100644
--- a/core/res/src/main/res/values-uk-rUA/strings.xml
+++ b/core/res/src/main/res/values-uk-rUA/strings.xml
@@ -916,6 +916,7 @@
Недопустима сума
Комісія перевищує залишок
Сума, що відправляється, перевищує залишок
+ Виберіть будь-який токен. Той, кому ви його надішлете, отримає саме те, що ви обрали — без жодних ускладнень.
Трансакцію надіслано
Підготуйтеся до сканування кільця або картки, яку ви хочете налаштувати.
Забути гаманець
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index d29deb2f04..e12369b185 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -530,6 +530,9 @@
Create Mobile Wallet
This recovery phrase has already been imported
Mobile Wallet
+ This device can’t be used for upgrade, it already contains another wallet.
+ Choose another device, this one can’t be used for upgrade.
+ An error occurred during the operation.
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
@@ -1059,7 +1062,7 @@
Confirm Conversion
Sending any other currency will result in its irreversible loss.
Select the correct recipient network
- Send any token, and we’ll convert it on the way. Your recipient gets exactly what they need—seamlessly.
+ Choose any token to receive. Your recipient gets exactly what you selected—seamlessly.
Recipient will receive
To recipient
Amount to receive
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 084ff6b3e5..e7f0ce0a1f 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
@@ -13,6 +13,8 @@ import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
+import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
+import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.wallets.PromoCodeActivationResult
import com.tangem.domain.wallets.PromoCodeActivationResult.*
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
@@ -39,6 +41,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
@Assisted private val queryParams: Map,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
+ private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val activateBitcoinPromocodeUseCase: ActivateBitcoinPromocodeUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val analyticsEventsHandler: AnalyticsEventHandler,
@@ -70,17 +73,38 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
private fun findBitcoinAddress(userWallet: UserWallet, promoCode: String) {
scope.launch(context = dispatchers.default) {
- val bitcoinStatus = withTimeoutOrNull(
+ val networkStatuses = withTimeoutOrNull(
FETCH_TIMEOUT_SECONDS.seconds,
{
multiNetworkStatusSupplier
.invoke(MultiNetworkStatusProducer.Params(userWallet.walletId))
.first { statuses ->
statuses.any { status -> status.network.rawId == Blockchain.Bitcoin.id }
- }.first { status -> status.network.rawId == Blockchain.Bitcoin.id }
+ }
},
)
+ Timber.tag(LOG_TAG).d("All user network statuses ${networkStatuses?.size}")
+
+ val cryptoCurrencies = multiWalletCryptoCurrenciesSupplier
+ .getSyncOrNull(
+ MultiWalletCryptoCurrenciesProducer.Params(
+ userWallet
+ .walletId,
+ ),
+ )
+
+ Timber.tag(LOG_TAG).d("All user cryptoCurrencies on main ${cryptoCurrencies?.size}")
+
+ val bitcoinCurrency = cryptoCurrencies?.firstOrNull { it.id.rawNetworkId == Blockchain.Bitcoin.id }
+ Timber.tag(LOG_TAG).d("BitcoinCurrency $bitcoinCurrency")
+
+ val bitcoinStatus = networkStatuses?.firstOrNull { status ->
+ status.network.id == bitcoinCurrency
+ ?.network?.id
+ }
+ Timber.tag(LOG_TAG).d("BitcoinStatus $bitcoinStatus")
+
if (bitcoinStatus == null) {
Timber.tag(LOG_TAG).d("No bitcoin, bitcoin network status == null")
showAlert(NoBitcoinAddress)
@@ -112,11 +136,13 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
private suspend fun activatePromoCode(bitcoinAddress: String, promoCode: String) {
uiMessageSender.send(GlobalLoadingMessage(true))
activateBitcoinPromocodeUseCase(bitcoinAddress, promoCode).onRight {
+ delay(DEFAULT_MESSAGE_SENDER_DELAY)
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 ->
+ delay(DEFAULT_MESSAGE_SENDER_DELAY)
uiMessageSender.send(GlobalLoadingMessage(false))
delay(DEFAULT_MESSAGE_SENDER_DELAY)
Timber.tag(LOG_TAG).d("${promoCode.mask()} activation failed $error")
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 024a9415a4..b37d43ee93 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
@@ -10,6 +10,7 @@ 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.StatusSource
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
@@ -17,6 +18,7 @@ import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
+import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.wallets.models.GetUserWalletError
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
import com.tangem.domain.wallets.usecase.ActivateBitcoinPromocodeUseCase
@@ -26,6 +28,7 @@ import com.tangem.feature.wallet.deeplink.analytics.PromoActivationAnalytics
import com.tangem.feature.wallet.impl.R
import io.mockk.MockKAnnotations
import io.mockk.coEvery
+import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.just
@@ -38,7 +41,6 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
-import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -57,6 +59,9 @@ class DefaultPromoDeeplinkHandlerTest {
@MockK
private lateinit var multiNetworkStatusSupplier: MultiNetworkStatusSupplier
+ @MockK
+ private lateinit var multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier
+
@MockK
private lateinit var activateBitcoinPromocodeUseCase: ActivateBitcoinPromocodeUseCase
@@ -92,6 +97,8 @@ class DefaultPromoDeeplinkHandlerTest {
emit(setOf(ethStatus))
emit(setOf(btcStatus))
}
+ val btcCoin = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id)
+ coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoin)
coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qxyz", promoCode) } returns Either.Right("ok")
val dispatcherProvider = testDispatcherProvider(testScheduler)
@@ -100,14 +107,13 @@ class DefaultPromoDeeplinkHandlerTest {
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
- advanceUntilIdle()
- advanceTimeBy(500)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
@@ -131,6 +137,7 @@ class DefaultPromoDeeplinkHandlerTest {
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
@@ -159,6 +166,7 @@ class DefaultPromoDeeplinkHandlerTest {
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
@@ -178,12 +186,14 @@ class DefaultPromoDeeplinkHandlerTest {
}
@Test
- fun `GIVEN no bitcoin address WHEN getWalletCurrenciesScreen THEN no bitcoin address dialog is shown`() = runTest {
+ fun `GIVEN no bitcoin address WHEN findBitcoinAddress 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)
val btcUnreachable = buildUnreachableNetworkStatus(rawNetworkId = Blockchain.Bitcoin.id)
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcUnreachable))
+ val btcCoin = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id)
+ coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoin)
val dispatcherProvider = testDispatcherProvider(testScheduler)
DefaultPromoDeeplinkHandler(
@@ -191,14 +201,13 @@ class DefaultPromoDeeplinkHandlerTest {
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
- advanceUntilIdle()
- advanceTimeBy(500)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
@@ -221,6 +230,8 @@ class DefaultPromoDeeplinkHandlerTest {
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
val btcStatus = buildNetworkStatus(rawNetworkId = Blockchain.Bitcoin.id, address = "bc1qxyz")
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatus))
+ val btcCoin = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id)
+ coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoin)
coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qxyz", promoCode) } returns Either.Right("ok")
val dispatcherProvider = testDispatcherProvider(testScheduler)
@@ -229,14 +240,13 @@ class DefaultPromoDeeplinkHandlerTest {
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
- advanceUntilIdle()
- advanceTimeBy(500)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
@@ -326,6 +336,8 @@ class DefaultPromoDeeplinkHandlerTest {
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
val btcStatus = buildNetworkStatus(rawNetworkId = Blockchain.Bitcoin.id, address = "bc1qxyz")
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatus))
+ val btcCurrency = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id)
+ coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCurrency)
coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qxyz", promoCode) } returns Either.Left(error)
val dispatcherProvider = testDispatcherProvider(testScheduler)
@@ -334,14 +346,13 @@ class DefaultPromoDeeplinkHandlerTest {
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
- advanceUntilIdle()
- advanceTimeBy(500)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
@@ -349,20 +360,418 @@ class DefaultPromoDeeplinkHandlerTest {
Truth.assertThat(sent.message).isEqualTo(expectedMessage)
}
+ @Test
+ fun `GIVEN BTC status but currencies without BTC WHEN findBitcoinAddress 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)
+ val btcStatus = buildNetworkStatus(rawNetworkId = Blockchain.Bitcoin.id, address = "bc1qxyz")
+ coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatus))
+ val ethOnly = buildCryptoCurrency(rawNetworkId = "ethereum")
+ coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(ethOnly)
+ val dispatcherProvider = testDispatcherProvider(testScheduler)
+
+ DefaultPromoDeeplinkHandler(
+ scope = this,
+ queryParams = queryParams,
+ uiMessageSender = uiMessageSender,
+ multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
+ activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
+ getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
+ analyticsEventsHandler = analyticsEventHandler,
+ dispatchers = dispatcherProvider,
+ )
+
+ advanceUntilIdle()
+
+ val sent = messages.last { it is DialogMessage } 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 multiple statuses emissions and currencies without BTC WHEN findBitcoinAddress THEN no bitcoin address 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 ethStatus = buildNetworkStatus(rawNetworkId = "ethereum", address = "0x123")
+ val btcStatus = buildNetworkStatus(rawNetworkId = Blockchain.Bitcoin.id, address = "bc1qxyz")
+ coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flow {
+ emit(emptySet())
+ emit(setOf(ethStatus))
+ emit(setOf(ethStatus, btcStatus))
+ }
+
+ val ethOnly = buildCryptoCurrency(rawNetworkId = "ethereum")
+ coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(ethOnly)
+
+ val dispatcherProvider = testDispatcherProvider(testScheduler)
+
+ DefaultPromoDeeplinkHandler(
+ scope = this,
+ queryParams = queryParams,
+ uiMessageSender = uiMessageSender,
+ multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
+ activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
+ getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
+ analyticsEventsHandler = analyticsEventHandler,
+ dispatchers = dispatcherProvider,
+ )
+
+ advanceUntilIdle()
+
+ val sent = messages.last { it is DialogMessage } 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 two BTC statuses with different derivation AND two BTC currencies WHEN activate THEN activated`() =
+ 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 dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
+ val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
+
+ val btcStatusCard = buildNetworkStatus(
+ rawNetworkId = Blockchain.Bitcoin.id,
+ address = "bc1qcard",
+ derivationPath = dpCard,
+ )
+ val btcStatusCustom = buildNetworkStatus(
+ rawNetworkId = Blockchain.Bitcoin.id,
+ address = "bc1qcustom",
+ derivationPath = dpCustom,
+ )
+ coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCard, btcStatusCustom))
+
+ val btcCoinCustom = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCustom)
+ val btcCoinCard = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCard)
+ coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(
+ btcCoinCustom,
+ btcCoinCard,
+ )
+ coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qcustom", promoCode) } returns Either.Right("ok")
+
+ val dispatcherProvider = testDispatcherProvider(testScheduler)
+
+ DefaultPromoDeeplinkHandler(
+ scope = this,
+ queryParams = queryParams,
+ uiMessageSender = uiMessageSender,
+ multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
+ activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
+ getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
+ analyticsEventsHandler = analyticsEventHandler,
+ dispatchers = dispatcherProvider,
+ )
+
+ advanceUntilIdle()
+
+ val sent = messages.last { it is DialogMessage } 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))
+
+ coVerify(exactly = 1) { activateBitcoinPromocodeUseCase.invoke("bc1qcustom", promoCode) }
+ coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke("bc1qcard", promoCode) }
+ }
+
+ @Test
+ fun `GIVEN two BTC statuses with different derivation AND one matching BTC currency WHEN activate THEN activated`() = 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 dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
+ val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
+
+ val btcStatusCard = buildNetworkStatus(
+ rawNetworkId = Blockchain.Bitcoin.id,
+ address = "bc1qcard",
+ derivationPath = dpCard,
+ )
+ val btcStatusCustom = buildNetworkStatus(
+ rawNetworkId = Blockchain.Bitcoin.id,
+ address = "bc1qcustom",
+ derivationPath = dpCustom,
+ )
+ coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCard, btcStatusCustom))
+
+ val btcCoinCard = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCard)
+ coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoinCard)
+
+ coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qcard", promoCode) } returns Either.Right("ok")
+
+ val dispatcherProvider = testDispatcherProvider(testScheduler)
+
+ DefaultPromoDeeplinkHandler(
+ scope = this,
+ queryParams = queryParams,
+ uiMessageSender = uiMessageSender,
+ multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
+ activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
+ getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
+ analyticsEventsHandler = analyticsEventHandler,
+ dispatchers = dispatcherProvider,
+ )
+
+ advanceUntilIdle()
+
+ val sent = messages.last { it is DialogMessage } 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))
+
+ coVerify(exactly = 1) { activateBitcoinPromocodeUseCase.invoke("bc1qcard", promoCode) }
+ coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke("bc1qcustom", promoCode) }
+
+ verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
+ verify(exactly = 1) {
+ analyticsEventHandler.send(
+ PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated),
+ )
+ }
+ }
+
+ @Test
+ fun `GIVEN two BTC statuses with different derivation AND no BTC currencies WHEN findBitcoinAddress THEN no bitcoin address 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 dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
+ val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
+
+ val btcStatusCard = buildNetworkStatus(
+ rawNetworkId = Blockchain.Bitcoin.id,
+ address = "bc1qcard",
+ derivationPath = dpCard,
+ )
+ val btcStatusCustom = buildNetworkStatus(
+ rawNetworkId = Blockchain.Bitcoin.id,
+ address = "bc1qcustom",
+ derivationPath = dpCustom,
+ )
+ coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCard, btcStatusCustom))
+
+ coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns emptySet()
+
+ val dispatcherProvider = testDispatcherProvider(testScheduler)
+
+ DefaultPromoDeeplinkHandler(
+ scope = this,
+ queryParams = queryParams,
+ uiMessageSender = uiMessageSender,
+ multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
+ activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
+ getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
+ analyticsEventsHandler = analyticsEventHandler,
+ dispatchers = dispatcherProvider,
+ )
+
+ advanceUntilIdle()
+
+ val sent = messages.last { it is DialogMessage } 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))
+
+ coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
+
+ verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
+ verify(exactly = 1) {
+ analyticsEventHandler.send(
+ PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
+ )
+ }
+ }
+
+ @Test
+ fun `GIVEN two BTC currencies first derivation mismatched AND one matching status WHEN findBitcoinAddress THEN no bitcoin address 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 dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
+ val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
+
+ val btcStatusCard = buildNetworkStatus(
+ rawNetworkId = Blockchain.Bitcoin.id,
+ address = "bc1qcard",
+ derivationPath = dpCard,
+ )
+ coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCard))
+
+ val btcCoinCustom = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCustom)
+ val btcCoinCard = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCard)
+ coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoinCustom, btcCoinCard)
+
+ val dispatcherProvider = testDispatcherProvider(testScheduler)
+
+ DefaultPromoDeeplinkHandler(
+ scope = this,
+ queryParams = queryParams,
+ uiMessageSender = uiMessageSender,
+ multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
+ activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
+ getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
+ analyticsEventsHandler = analyticsEventHandler,
+ dispatchers = dispatcherProvider,
+ )
+
+ advanceUntilIdle()
+
+ val sent = messages.last { it is DialogMessage } 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))
+
+ coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
+
+ verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
+ verify(exactly = 1) {
+ analyticsEventHandler.send(
+ PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
+ )
+ }
+ }
+
+ @Test
+ fun `GIVEN only BTC status with CUSTOM derivation AND only BTC currency with CARD derivation WHEN findBitcoinAddress THEN no bitcoin address 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 dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
+ val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
+
+ val btcStatusCustom = buildNetworkStatus(
+ rawNetworkId = Blockchain.Bitcoin.id,
+ address = "bc1qcustom",
+ derivationPath = dpCustom,
+ )
+ coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCustom))
+
+ val btcCoinCard = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCard)
+ coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoinCard)
+
+ val dispatcherProvider = testDispatcherProvider(testScheduler)
+
+ DefaultPromoDeeplinkHandler(
+ scope = this,
+ queryParams = queryParams,
+ uiMessageSender = uiMessageSender,
+ multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
+ activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
+ getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
+ analyticsEventsHandler = analyticsEventHandler,
+ dispatchers = dispatcherProvider,
+ )
+
+ advanceUntilIdle()
+
+ val sent = messages.last { it is DialogMessage } 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))
+
+ coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
+
+ verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
+ verify(exactly = 1) {
+ analyticsEventHandler.send(
+ PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
+ )
+ }
+ }
+
+ @Test
+ fun `GIVEN only BTC status with CARD derivation AND only BTC currency with CUSTOM derivation WHEN findBitcoinAddress THEN no bitcoin address 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 dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
+ val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
+
+ val btcStatusCard = buildNetworkStatus(
+ rawNetworkId = Blockchain.Bitcoin.id,
+ address = "bc1qcard",
+ derivationPath = dpCard,
+ )
+ coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCard))
+
+ val btcCoinCustom = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCustom)
+ coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoinCustom)
+
+ val dispatcherProvider = testDispatcherProvider(testScheduler)
+
+ DefaultPromoDeeplinkHandler(
+ scope = this,
+ queryParams = queryParams,
+ uiMessageSender = uiMessageSender,
+ multiNetworkStatusSupplier = multiNetworkStatusSupplier,
+ multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
+ activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
+ getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
+ analyticsEventsHandler = analyticsEventHandler,
+ dispatchers = dispatcherProvider,
+ )
+
+ advanceUntilIdle()
+
+ val sent = messages.last { it is DialogMessage } 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))
+
+ coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
+
+ verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
+ verify(exactly = 1) {
+ analyticsEventHandler.send(
+ PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
+ )
+ }
+ }
+
private fun mockUserWallet(id: String): UserWallet {
val userWallet = mockk(relaxed = true)
every { userWallet.walletId } returns UserWalletId(id)
return userWallet
}
- private fun buildNetworkStatus(rawNetworkId: String, address: String): NetworkStatus {
- val networkId = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None)
+ private fun buildNetworkStatus(
+ rawNetworkId: String,
+ address: String,
+ derivationPath: Network.DerivationPath = Network.DerivationPath.None,
+ ): NetworkStatus {
+ val networkId = Network.ID(Network.RawID(rawNetworkId), derivationPath)
val network = Network(
id = networkId,
backendId = rawNetworkId,
name = rawNetworkId,
currencySymbol = rawNetworkId.take(3).uppercase(),
- derivationPath = Network.DerivationPath.None,
+ derivationPath = derivationPath,
isTestnet = false,
standardType = Network.StandardType.Unspecified("UNSPECIFIED"),
hasFiatFeeRate = false,
@@ -405,6 +814,39 @@ class DefaultPromoDeeplinkHandlerTest {
return NetworkStatus(network = network, value = value)
}
+ private fun buildCryptoCurrency(
+ rawNetworkId: String,
+ derivationPath: Network.DerivationPath = Network.DerivationPath.None,
+ ): CryptoCurrency.Coin {
+ val networkId = Network.ID(Network.RawID(rawNetworkId), derivationPath)
+ val network = Network(
+ id = networkId,
+ backendId = rawNetworkId,
+ name = rawNetworkId,
+ currencySymbol = rawNetworkId.take(3).uppercase(),
+ derivationPath = derivationPath,
+ isTestnet = false,
+ standardType = Network.StandardType.Unspecified("UNSPECIFIED"),
+ hasFiatFeeRate = false,
+ canHandleTokens = false,
+ transactionExtrasType = Network.TransactionExtrasType.NONE,
+ )
+
+ return CryptoCurrency.Coin(
+ id = CryptoCurrency.ID(
+ prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
+ body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId),
+ suffix = CryptoCurrency.ID.Suffix.RawID(rawNetworkId),
+ ),
+ network = network,
+ name = rawNetworkId,
+ symbol = rawNetworkId.take(3).uppercase(),
+ decimals = 8,
+ iconUrl = null,
+ isCustom = false,
+ )
+ }
+
private fun testDispatcherProvider(scheduler: TestCoroutineScheduler): CoroutineDispatcherProvider {
val dispatcher: CoroutineDispatcher = StandardTestDispatcher(scheduler)
return object : CoroutineDispatcherProvider {