Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-29 14:00:38 +03:00
parent fb597aa015
commit e557bc44e6
6 changed files with 499 additions and 17 deletions

View file

@ -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<String, String>,
@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")

View file

@ -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<UserWallet>(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 {