Updated on 2026-08-14
This commit is contained in:
parent
db28e42f72
commit
25444c3a2d
17 changed files with 415 additions and 50 deletions
|
|
@ -672,7 +672,10 @@ internal class ChildFactory @Inject constructor(
|
|||
is AppRoute.TangemPayDetails -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = TangemPayDetailsContainerComponent.Params(initialStatus = route.status),
|
||||
params = TangemPayDetailsContainerComponent.Params(
|
||||
initialStatus = route.status,
|
||||
initialRoute = route.initialRoute,
|
||||
),
|
||||
componentFactory = tangemPayDetailsContainerComponentFactory,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.tangem.domain.models.account.AccountId
|
|||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.earn.PreselectedEarnType
|
||||
import com.tangem.domain.models.pay.TangemPayDetailsInitialRoute
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -487,6 +488,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
@Serializable
|
||||
data class TangemPayDetails(
|
||||
val status: AccountStatus.Payment,
|
||||
val initialRoute: TangemPayDetailsInitialRoute = TangemPayDetailsInitialRoute.ACCOUNT_DETAILS,
|
||||
) : AppRoute(path = "/tangem_pay_details/${status.account}")
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ dependencies {
|
|||
// region Project - Features
|
||||
api(projects.features.swap.domain)
|
||||
api(projects.features.virtualAccounts.details.api)
|
||||
api(projects.features.tangempay.details.api)
|
||||
// endregion
|
||||
|
||||
// region Project - Libs
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.SpecificationDat
|
|||
import com.tangem.domain.pay.model.OrderData
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.model.TangemPayEntryPoint
|
||||
import com.tangem.domain.pay.usecase.GetTangemPayTariffPlanStateUseCase
|
||||
import com.tangem.domain.pay.repository.*
|
||||
import com.tangem.domain.pay.usecase.GetTangemPayTariffPlanStateUseCase
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import com.tangem.security.isSecurityExposed
|
||||
|
|
@ -64,6 +65,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
private val issueCardRepository: TangemPayIssueCardRepository,
|
||||
private val virtualAccountFeatureToggles: VirtualAccountFeatureToggles,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
private val getTangemPayTariffPlanStateUseCase: GetTangemPayTariffPlanStateUseCase,
|
||||
) : PaymentAccountStatusFetcher {
|
||||
|
||||
|
|
@ -174,17 +176,65 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
},
|
||||
ifRight = { customerInfo ->
|
||||
logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}")
|
||||
val status = customerInfo.mapToPaymentAccountStatus(account.userWalletId)
|
||||
if (status is PaymentAccountStatusValue.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) {
|
||||
// If order id wasn't saved -> start order creation and get customer info
|
||||
onboardingRepository.createOrder(account.userWalletId)
|
||||
.onLeft { TangemLogger.withTag(TAG).e("createOrder failed: $it") }
|
||||
}
|
||||
status
|
||||
resolveFinalStatus(
|
||||
account = account,
|
||||
customerInfo = customerInfo,
|
||||
status = customerInfo.mapToPaymentAccountStatus(account.userWalletId),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun resolveFinalStatus(
|
||||
account: Account.Payment,
|
||||
customerInfo: CustomerInfo,
|
||||
status: PaymentAccountStatusValue,
|
||||
): PaymentAccountStatusValue {
|
||||
val isIssuing = status is PaymentAccountStatusValue.IssuingCard
|
||||
val isApproved = customerInfo.kycStatus == KycStatus.APPROVED
|
||||
val userWalletId = account.userWalletId
|
||||
val tariffPlan = customerInfo.tariffPlan
|
||||
|
||||
if (!tangemPayFeatureToggles.isTiersPlusPlanEnabled) {
|
||||
if (isIssuing && isApproved) {
|
||||
// If order id wasn't saved -> start order creation and get customer info
|
||||
onboardingRepository.createOrder(userWalletId)
|
||||
.onLeft { logger.e("createOrder failed: $it") }
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
if (!isIssuing || !isApproved) {
|
||||
return status
|
||||
}
|
||||
|
||||
if (tariffPlan == null) {
|
||||
return PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
}
|
||||
|
||||
val hasActiveIssueOrder = issueCardRepository.getIssueOrderIds(userWalletId).isNotEmpty()
|
||||
return if (hasActiveIssueOrder) {
|
||||
PaymentAccountStatusValue.Inactive(
|
||||
source = StatusSource.ACTUAL,
|
||||
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
|
||||
tariffPlan = getTangemPayTariffPlanStateUseCase(
|
||||
userWalletId = userWalletId,
|
||||
tariff = tariffPlan,
|
||||
),
|
||||
fiatBalance = PaymentAccountStatusValue.FiatBalance(
|
||||
availableBalance = BigDecimal.ZERO,
|
||||
currency = tariffPlan.plan.feeCurrencyOrDefault(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
PaymentAccountStatusValue.AwaitingPlanSelection(
|
||||
source = StatusSource.ACTUAL,
|
||||
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
|
||||
tariffPlan = tariffPlan,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithOrderId(account: Account.Payment, orderId: String): PaymentAccountStatusValue {
|
||||
// Step 1: Check KYC status first
|
||||
val customerInfo = onboardingRepository.getCustomerInfo(account.userWalletId).fold(
|
||||
|
|
@ -286,24 +336,26 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
params = SingleQuoteStatusProducer.Params(rawCurrencyId = TangemPayCurrencyFactory.TOKEN_ID),
|
||||
)?.value as? QuoteStatus.Data
|
||||
|
||||
val customerId = customerId
|
||||
val isDeactivated = productInstance?.status == CustomerInfo.ProductInstance.Status.DEACTIVATED
|
||||
val isFormer = state == CustomerInfo.State.FORMER
|
||||
val fiatBalance = fiatBalance
|
||||
val cryptoBalance = cryptoBalance
|
||||
|
||||
val hasCardData = cards.isNotEmpty() && productInstances.isNotEmpty()
|
||||
val isTiersPlusPlanEnabled = tangemPayFeatureToggles.isTiersPlusPlanEnabled
|
||||
return when {
|
||||
kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty() -> {
|
||||
PaymentAccountStatusValue.UnderReview(
|
||||
source = StatusSource.ACTUAL,
|
||||
kycStatus = kycStatus,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
)
|
||||
}
|
||||
fiatBalance != null && cryptoBalance != null && !customerId.isNullOrEmpty() &&
|
||||
(isDeactivated || isFormer) -> {
|
||||
customerId.isNullOrEmpty() -> PaymentAccountStatusValue.IssuingCard(
|
||||
source = StatusSource.ACTUAL,
|
||||
)
|
||||
kycStatus != KycStatus.APPROVED -> PaymentAccountStatusValue.UnderReview(
|
||||
source = StatusSource.ACTUAL,
|
||||
kycStatus = kycStatus,
|
||||
customerId = customerId,
|
||||
)
|
||||
fiatBalance != null && cryptoBalance != null && (isDeactivated || isFormer) ->
|
||||
PaymentAccountStatusValue.Deactivated(
|
||||
source = StatusSource.ACTUAL,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
customerId = customerId,
|
||||
balance = PaymentAccountStatusValue.Balance(
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
|
|
@ -313,15 +365,14 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
fiatRate = quotesData?.fiatRate,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
cards.isNotEmpty() && productInstances.isNotEmpty() &&
|
||||
fiatBalance != null && cryptoBalance != null && !customerId.isNullOrEmpty() -> convertToContentState(
|
||||
userWalletId = userWalletId,
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
fiatRate = quotesData?.fiatRate,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
)
|
||||
fiatBalance != null && cryptoBalance != null && (hasCardData || isTiersPlusPlanEnabled) ->
|
||||
convertToContentState(
|
||||
userWalletId = userWalletId,
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
fiatRate = quotesData?.fiatRate,
|
||||
customerId = customerId,
|
||||
)
|
||||
else -> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
}
|
||||
}
|
||||
|
|
@ -365,7 +416,9 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
if (tangemPayCards.isEmpty()) return PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
if (!tangemPayFeatureToggles.isTiersPlusPlanEnabled && tangemPayCards.isEmpty()) {
|
||||
return PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
}
|
||||
|
||||
// Additional-card issuance: the backend omits the new card until it is provisioned, so surface a
|
||||
// placeholder for every locally tracked in-flight issuance order alongside the real cards.
|
||||
|
|
@ -376,6 +429,12 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
// the previously shown order and append newly seen cards at the end.
|
||||
val orderedCards = tangemPayCards.stableOrder(previousRealCardOrder(userWalletId))
|
||||
|
||||
val allCards = orderedCards + issuingCards
|
||||
|
||||
if (allCards.isEmpty()) {
|
||||
return PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
}
|
||||
|
||||
val virtualAccount = resolveVirtualAccountOnramp(userWalletId)
|
||||
|
||||
return PaymentAccountStatusValue.Loaded(
|
||||
|
|
@ -384,7 +443,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
depositAddress = cryptoBalance.depositAddress,
|
||||
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
|
||||
fiatRate = fiatRate,
|
||||
cards = orderedCards + issuingCards,
|
||||
cards = allCards,
|
||||
balance = PaymentAccountStatusValue.Balance(
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,15 @@ import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
|||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.BankCredentials
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlanState
|
||||
import com.tangem.domain.models.account.VirtualAccountOnramp
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
|
|
@ -22,6 +26,7 @@ import com.tangem.domain.pay.repository.*
|
|||
import com.tangem.domain.pay.usecase.GetTangemPayTariffPlanStateUseCase
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
|
|
@ -49,6 +54,7 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
private val cardDetailsRepository: TangemPayCardDetailsRepository = mockk()
|
||||
private val issueCardRepository: TangemPayIssueCardRepository = mockk()
|
||||
private val virtualAccountFeatureToggles: VirtualAccountFeatureToggles = mockk()
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles = mockk()
|
||||
private val getTangemPayTariffPlanStateUseCase: GetTangemPayTariffPlanStateUseCase = mockk()
|
||||
|
||||
private val fetcher = DefaultPaymentAccountStatusFetcher(
|
||||
|
|
@ -65,6 +71,7 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
cardDetailsRepository = cardDetailsRepository,
|
||||
issueCardRepository = issueCardRepository,
|
||||
virtualAccountFeatureToggles = virtualAccountFeatureToggles,
|
||||
tangemPayFeatureToggles = tangemPayFeatureToggles,
|
||||
getTangemPayTariffPlanStateUseCase = getTangemPayTariffPlanStateUseCase,
|
||||
)
|
||||
|
||||
|
|
@ -111,27 +118,47 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
images = emptyList(),
|
||||
)
|
||||
|
||||
private val basicPlan = TangemPayTariffPlan(
|
||||
id = "plan_basic",
|
||||
type = TangemPayTariffPlan.Type.BASIC,
|
||||
name = "Basic",
|
||||
descriptionItems = emptyList(),
|
||||
images = emptyList(),
|
||||
fees = emptyList(),
|
||||
)
|
||||
|
||||
private val customerTariffPlan = TangemPayCustomerTariffPlan(
|
||||
status = TangemPayCustomerTariffPlan.Status.ACTIVE,
|
||||
plan = basicPlan,
|
||||
nextBillingAt = null,
|
||||
pendingPlan = null,
|
||||
pendingTransitionAt = null,
|
||||
)
|
||||
|
||||
private fun buildCustomerInfo(
|
||||
productInstances: List<CustomerInfo.ProductInstance> = listOf(cardProductInstance),
|
||||
) = CustomerInfo(
|
||||
customerId = "cust_1",
|
||||
kycStatus = KycStatus.APPROVED,
|
||||
state = CustomerInfo.State.ACTIVE,
|
||||
fiatBalance = PaymentAccountStatusValue.FiatBalance(
|
||||
fiatBalance: PaymentAccountStatusValue.FiatBalance? = PaymentAccountStatusValue.FiatBalance(
|
||||
availableBalance = BigDecimal.TEN,
|
||||
currency = "USD",
|
||||
),
|
||||
cryptoBalance = PaymentAccountStatusValue.CryptoBalance(
|
||||
cryptoBalance: PaymentAccountStatusValue.CryptoBalance? = PaymentAccountStatusValue.CryptoBalance(
|
||||
id = "usdc",
|
||||
chainId = 137L,
|
||||
depositAddress = "0xdeposit",
|
||||
tokenContractAddress = "0xcontract",
|
||||
balance = BigDecimal.TEN,
|
||||
),
|
||||
tariffPlan: TangemPayCustomerTariffPlan? = null,
|
||||
) = CustomerInfo(
|
||||
customerId = "cust_1",
|
||||
kycStatus = KycStatus.APPROVED,
|
||||
state = CustomerInfo.State.ACTIVE,
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
availableForWithdrawal = BigDecimal.TEN,
|
||||
cards = listOf(cardInfo),
|
||||
productInstances = productInstances,
|
||||
tariffPlan = null,
|
||||
tariffPlan = tariffPlan,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
|
|
@ -146,10 +173,14 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
cardDetailsRepository,
|
||||
issueCardRepository,
|
||||
virtualAccountFeatureToggles,
|
||||
tangemPayFeatureToggles,
|
||||
getTangemPayTariffPlanStateUseCase,
|
||||
)
|
||||
// Relaxed mocks don't need clearing — deviceSecurity, eligibilityManager, paymentAccountStatusesStore
|
||||
// are relaxed and consistent with their relaxed defaults (false, empty, etc.)
|
||||
clearMocks(paymentAccountStatusesStore, answers = false)
|
||||
// Tiers off by default — legacy auto-order-creation behavior. Individual tests override.
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns false
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -304,4 +335,142 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
assertThat(loaded.virtualAccount).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class TiersPlanSelection {
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers on and KYC approved and no plan order WHEN invoke THEN stores AwaitingPlanSelection`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val customerInfo = buildCustomerInfo(
|
||||
productInstances = emptyList(),
|
||||
fiatBalance = null,
|
||||
cryptoBalance = null,
|
||||
tariffPlan = customerTariffPlan,
|
||||
)
|
||||
stubHappyPath(customerInfo)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns true
|
||||
coEvery { issueCardRepository.getIssueOrderIds(userWalletId) } returns emptyList()
|
||||
val storedStatuses = captureStoredStatuses()
|
||||
|
||||
// Act
|
||||
fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
assertThat(storedStatuses.last().value)
|
||||
.isInstanceOf(PaymentAccountStatusValue.AwaitingPlanSelection::class.java)
|
||||
coVerify(exactly = 0) { onboardingRepository.createOrder(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers on and fallback plan is missing WHEN invoke THEN stores IssuingCard without order`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val customerInfo = buildCustomerInfo(
|
||||
productInstances = emptyList(),
|
||||
fiatBalance = null,
|
||||
cryptoBalance = null,
|
||||
tariffPlan = null,
|
||||
)
|
||||
stubHappyPath(customerInfo)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns true
|
||||
val storedStatuses = captureStoredStatuses()
|
||||
|
||||
// Act
|
||||
fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
assertThat(storedStatuses.last().value)
|
||||
.isInstanceOf(PaymentAccountStatusValue.IssuingCard::class.java)
|
||||
coVerify(exactly = 0) { onboardingRepository.createOrder(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers on and plan selected but no balance yet WHEN invoke THEN stores Inactive`() = runTest {
|
||||
// Arrange
|
||||
val customerInfo = buildCustomerInfo(
|
||||
productInstances = emptyList(),
|
||||
fiatBalance = null,
|
||||
cryptoBalance = null,
|
||||
tariffPlan = customerTariffPlan,
|
||||
)
|
||||
stubHappyPath(customerInfo)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns true
|
||||
coEvery { issueCardRepository.getIssueOrderIds(userWalletId) } returns listOf("order_1")
|
||||
coEvery {
|
||||
getTangemPayTariffPlanStateUseCase(userWalletId = userWalletId, tariff = customerTariffPlan)
|
||||
} returns TangemPayTariffPlanState(tariff = customerTariffPlan, order = null)
|
||||
val storedStatuses = captureStoredStatuses()
|
||||
|
||||
// Act
|
||||
fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
assertThat(storedStatuses.last().value)
|
||||
.isInstanceOf(PaymentAccountStatusValue.Inactive::class.java)
|
||||
coVerify(exactly = 0) { onboardingRepository.createOrder(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers off and KYC approved without card WHEN invoke THEN creates order and stays issuing`() = runTest {
|
||||
// Arrange
|
||||
val customerInfo = buildCustomerInfo(productInstances = emptyList())
|
||||
stubHappyPath(customerInfo)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns false
|
||||
coEvery { onboardingRepository.createOrder(userWalletId) } returns Either.Right("order_1")
|
||||
val storedStatuses = captureStoredStatuses()
|
||||
|
||||
// Act
|
||||
fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
assertThat(storedStatuses.last().value)
|
||||
.isInstanceOf(PaymentAccountStatusValue.IssuingCard::class.java)
|
||||
coVerify(exactly = 1) { onboardingRepository.createOrder(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers off and balance without instances but local issue order WHEN invoke THEN stores IssuingCard`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val customerInfo = buildCustomerInfo(productInstances = emptyList())
|
||||
stubHappyPath(customerInfo)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns false
|
||||
coEvery { issueCardRepository.getIssueOrderIds(userWalletId) } returns listOf("order_1")
|
||||
coEvery { onboardingRepository.createOrder(userWalletId) } returns Either.Right("order_1")
|
||||
val storedStatuses = captureStoredStatuses()
|
||||
|
||||
// Act
|
||||
fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
assertThat(storedStatuses.last().value)
|
||||
.isInstanceOf(PaymentAccountStatusValue.IssuingCard::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers on and balance without instances but local issue order WHEN invoke THEN stores Loaded with placeholder`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val customerInfo = buildCustomerInfo(productInstances = emptyList())
|
||||
stubHappyPath(customerInfo)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns true
|
||||
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns false
|
||||
coEvery { issueCardRepository.getIssueOrderIds(userWalletId) } returns listOf("order_1")
|
||||
coEvery {
|
||||
cardDetailsRepository.getOrderInfo(userWalletId, "order_1")
|
||||
} returns VisaApiError.UnknownWithoutCode.left()
|
||||
val storedStatuses = captureStoredStatuses()
|
||||
|
||||
// Act
|
||||
fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
val loaded = storedStatuses.lastLoaded()
|
||||
assertThat(loaded.cards).hasSize(1)
|
||||
assertThat(loaded.cards.single().state).isEqualTo(TangemPayCardState.Issuing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -149,4 +149,8 @@ data class TangemPayTariffPlan(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun TangemPayTariffPlan.feeCurrencyOrDefault(defaultCurrencyCode: String = "USD"): String {
|
||||
return fees.firstOrNull()?.currency ?: defaultCurrencyCode
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.domain.models.pay
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Which inner screen the Tangem Pay should open on when launched.
|
||||
*
|
||||
* [ACCOUNT_DETAILS] — the default account/main page.
|
||||
* [SELECT_PLAN] — the tariff-plan selection screen, used by the "Select plan" entry point on the
|
||||
* wallet main screen (Tiers).
|
||||
*/
|
||||
@Serializable
|
||||
enum class TangemPayDetailsInitialRoute {
|
||||
ACCOUNT_DETAILS,
|
||||
SELECT_PLAN,
|
||||
}
|
||||
|
|
@ -3,8 +3,12 @@ package com.tangem.features.tangempay.components
|
|||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.pay.TangemPayDetailsInitialRoute
|
||||
|
||||
interface TangemPayDetailsContainerComponent : ComposableContentComponent {
|
||||
data class Params(val initialStatus: AccountStatus.Payment)
|
||||
data class Params(
|
||||
val initialStatus: AccountStatus.Payment,
|
||||
val initialRoute: TangemPayDetailsInitialRoute,
|
||||
)
|
||||
interface Factory : ComponentFactory<Params, TangemPayDetailsContainerComponent>
|
||||
}
|
||||
|
|
@ -15,11 +15,13 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.pay.TangemPayDetailsInitialRoute
|
||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||
import com.tangem.features.tangempay.cashback.api.TangemPayCashbackComponent
|
||||
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
|
||||
import com.tangem.features.tangempay.tiers.current.TangemPayCurrentPlanComponent
|
||||
import com.tangem.features.tangempay.tiers.select.TangemPaySelectPlanComponent
|
||||
import com.tangem.features.tangempay.utils.tariffPlan
|
||||
import com.tangem.features.tangempay.utils.userWalletId
|
||||
import com.tangem.features.tokendetails.ExpressTransactionsComponent
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
|
|
@ -51,10 +53,22 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru
|
|||
key = "tangemPayDetailsInnerStack",
|
||||
source = stackNavigation,
|
||||
serializer = TangemPayAccountDetailsInnerRoute.serializer(),
|
||||
initialConfiguration = TangemPayAccountDetailsInnerRoute.AccountDetails,
|
||||
initialConfiguration = resolveInitialConfiguration(),
|
||||
childFactory = ::screenChild,
|
||||
)
|
||||
|
||||
private fun resolveInitialConfiguration(): TangemPayAccountDetailsInnerRoute {
|
||||
val tariffPlan = params.initialStatus.tariffPlan
|
||||
return when (params.initialRoute) {
|
||||
TangemPayDetailsInitialRoute.ACCOUNT_DETAILS -> TangemPayAccountDetailsInnerRoute.AccountDetails
|
||||
TangemPayDetailsInitialRoute.SELECT_PLAN -> if (tariffPlan != null) {
|
||||
TangemPayAccountDetailsInnerRoute.SelectPlan(tariffPlan = tariffPlan)
|
||||
} else {
|
||||
TangemPayAccountDetailsInnerRoute.AccountDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val childStack by childStack.subscribeAsState()
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory
|
|||
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
|
||||
import com.tangem.features.tangempay.model.transformers.*
|
||||
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
|
||||
import com.tangem.features.tangempay.tiers.feeCurrency
|
||||
import com.tangem.features.tangempay.utils.*
|
||||
import com.tangem.features.tokendetails.ExpressTransactionsEvent
|
||||
import com.tangem.features.tokendetails.ExpressTransactionsEventListener
|
||||
|
|
|
|||
|
|
@ -9,9 +9,6 @@ import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
|||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import org.joda.time.format.DateTimeFormatter
|
||||
|
||||
val TangemPayTariffPlan.feeCurrency: String?
|
||||
get() = fees.firstOrNull()?.currency
|
||||
|
||||
fun TangemPayTariffPlan.formatRecurringFeeOrNull(): String? {
|
||||
val fee = fees.find { it.type == TangemPayTariffPlan.Fee.Type.RECURRING } ?: return null
|
||||
val currency = getJavaCurrencyByCode(fee.currency)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ internal class TangemPaySelectPlanModel @Inject constructor(
|
|||
private val allowedTransitions: List<TangemPayTariffPlanTransition>
|
||||
get() = transitions.filter { it.type in ALLOWED_TYPES }
|
||||
|
||||
private val allowedTransitionsForCompare: List<TangemPayTariffPlanTransition>
|
||||
get() = transitions.filter { it.type in ALLOWED_TYPES_FOR_COMPARE }
|
||||
|
||||
private var selectedIndex: Int = 0
|
||||
private var isConfirm: Boolean = false
|
||||
private var isProcessing: Boolean = false
|
||||
|
|
@ -130,7 +133,7 @@ internal class TangemPaySelectPlanModel @Inject constructor(
|
|||
state.update { buildState() }
|
||||
modelScope.launch {
|
||||
action().fold(
|
||||
ifRight = { router.popTo(TangemPayAccountDetailsInnerRoute.AccountDetails) },
|
||||
ifRight = { router.replaceAll(TangemPayAccountDetailsInnerRoute.AccountDetails) },
|
||||
ifLeft = {
|
||||
isProcessing = false
|
||||
state.update { buildState() }
|
||||
|
|
@ -163,7 +166,7 @@ internal class TangemPaySelectPlanModel @Inject constructor(
|
|||
)
|
||||
|
||||
private fun buildCompare(): TangemPaySelectPlanUM.ComparePlans {
|
||||
val plans = listOf(params.tariffPlan.plan) + transitions.map { it.plan }
|
||||
val plans = listOf(params.tariffPlan.plan) + allowedTransitionsForCompare.map { it.plan }
|
||||
val orderedTitles = plans
|
||||
.flatMap { plan -> plan.descriptionItems.filter { it.section in COMPARE_SECTIONS } }
|
||||
.sortedWith(compareBy({ it.section.ordinal }, { it.order }))
|
||||
|
|
@ -282,6 +285,10 @@ internal class TangemPaySelectPlanModel @Inject constructor(
|
|||
TangemPayTariffPlanTransition.Type.DOWNGRADE,
|
||||
TangemPayTariffPlanTransition.Type.ACTIVATION,
|
||||
)
|
||||
private val ALLOWED_TYPES_FOR_COMPARE = setOf(
|
||||
TangemPayTariffPlanTransition.Type.UPGRADE,
|
||||
TangemPayTariffPlanTransition.Type.DOWNGRADE,
|
||||
)
|
||||
private val COMPARE_SECTIONS = setOf(
|
||||
TangemPayTariffPlan.Section.CARD_RELATED,
|
||||
TangemPayTariffPlan.Section.PLAN_RELATED,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.tangempay.utils
|
|||
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
|
|
@ -17,6 +18,15 @@ internal val AccountStatus.Payment.cryptoCurrency: CryptoCurrency.Token
|
|||
else -> error("TangemPayDetails opened with unsupported status: $v")
|
||||
}
|
||||
|
||||
internal val AccountStatus.Payment.tariffPlan: TangemPayCustomerTariffPlan?
|
||||
get() = when (val v = value) {
|
||||
is PaymentAccountStatusValue.Inactive -> v.tariffPlan.tariff
|
||||
is PaymentAccountStatusValue.AwaitingPlanSelection -> v.tariffPlan
|
||||
is PaymentAccountStatusValue.Loaded -> v.tariffPlan?.tariff
|
||||
is PaymentAccountStatusValue.Deactivated -> null
|
||||
else -> error("TangemPayDetails opened with unsupported status: $v")
|
||||
}
|
||||
|
||||
internal val AccountStatus.Payment.isDeactivated: Boolean
|
||||
get() = value is PaymentAccountStatusValue.Deactivated
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.domain.models.StatusSource
|
|||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayDetailsInitialRoute
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
|
|
@ -89,7 +90,10 @@ internal class TangemPayDetailsModelTest {
|
|||
every { userWalletId } returns this@TangemPayDetailsModelTest.userWalletId
|
||||
}
|
||||
}
|
||||
val params = TangemPayDetailsContainerComponent.Params(initialStatus = paymentStatus)
|
||||
val params = TangemPayDetailsContainerComponent.Params(
|
||||
initialStatus = paymentStatus,
|
||||
initialRoute = TangemPayDetailsInitialRoute.ACCOUNT_DETAILS,
|
||||
)
|
||||
|
||||
every { paymentAccountStatusSupplier.invoke(any<UserWalletId>()) } returns flowOf(paymentStatus)
|
||||
every { cardDetailsRepository.cardFrozenState(any()) } returns flowOf(frozenState)
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@ import com.tangem.core.navigation.url.UrlOpener
|
|||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.TangemPayEntryPoint
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.features.tangempay.TangemPayConstants
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
|
||||
import com.tangem.features.tangempay.components.WalletSelectorListener
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButtonLoadingTransformer
|
||||
|
|
@ -46,6 +48,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
|||
private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val eligibilityManager: TangemPayEligibilityManager,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
) : Model(), WalletSelectorListener {
|
||||
|
||||
private val params = paramsContainer.require<TangemPayOnboardingComponent.Params>()
|
||||
|
|
@ -118,7 +121,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
|||
.onRight { customerInfo ->
|
||||
when {
|
||||
customerInfo.kycStatus != KycStatus.APPROVED -> {
|
||||
if (customerInfo.productInstance == null) {
|
||||
if (shouldCreateOrderBeforeKyc(customerInfo)) {
|
||||
repository.createOrder(userWalletId)
|
||||
.onLeft { error ->
|
||||
TangemLogger.e("Error creating order before KYC: $error")
|
||||
|
|
@ -209,7 +212,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
|||
if (customerInfo.kycStatus == KycStatus.APPROVED) {
|
||||
back()
|
||||
} else {
|
||||
if (customerInfo.productInstance == null) {
|
||||
if (shouldCreateOrderBeforeKyc(customerInfo)) {
|
||||
repository.createOrder(userWalletId)
|
||||
.onLeft { error ->
|
||||
TangemLogger.e("Error creating order before KYC: $error")
|
||||
|
|
@ -222,6 +225,9 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun shouldCreateOrderBeforeKyc(customerInfo: CustomerInfo): Boolean =
|
||||
!tangemPayFeatureToggles.isTiersPlusPlanEnabled && customerInfo.productInstance == null
|
||||
|
||||
private fun openKyc(userWalletId: UserWalletId) {
|
||||
router.replaceAll(
|
||||
AppRoute.Wallet,
|
||||
|
|
|
|||
|
|
@ -9,17 +9,23 @@ import com.google.common.truth.Truth.assertThat
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.TangemPayEntryPoint
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
|
||||
import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
|
|
@ -33,6 +39,7 @@ import org.junit.jupiter.api.Test
|
|||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TangemPayOnboardingModelTest {
|
||||
|
|
@ -43,12 +50,14 @@ internal class TangemPayOnboardingModelTest {
|
|||
private val analytics: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase = mockk(relaxed = true)
|
||||
private val urlOpener: UrlOpener = mockk(relaxed = true)
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles = mockk()
|
||||
|
||||
private val deeplink = "tangem://onboard-visa"
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(router, repository, eligibilityManager, analytics, produceInitialDataUseCase, urlOpener)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns false
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
|
|
@ -166,6 +175,61 @@ internal class TangemPayOnboardingModelTest {
|
|||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers off and KYC not approved WHEN onboarding starts THEN order created before KYC`() = runTest {
|
||||
// Arrange
|
||||
val userWalletId = UserWalletId("011")
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns false
|
||||
coEvery { produceInitialDataUseCase(userWalletId) } returns Unit.right()
|
||||
coEvery { repository.getCustomerInfo(userWalletId) } returns
|
||||
buildCustomerInfo(kycStatus = KycStatus.PENDING).right()
|
||||
coEvery { repository.createOrder(userWalletId) } returns "order_1".right()
|
||||
|
||||
// Act
|
||||
val model = createModel(TangemPayOnboardingComponent.Params.HotWalletOnboarding(userWalletId))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { repository.createOrder(userWalletId) }
|
||||
verify(exactly = 1) {
|
||||
router.replaceAll(AppRoute.Wallet, AppRoute.Kyc(userWalletId = userWalletId), onComplete = any())
|
||||
}
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers on and KYC not approved WHEN onboarding starts THEN order not created before KYC`() = runTest {
|
||||
// Arrange
|
||||
val userWalletId = UserWalletId("011")
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns true
|
||||
coEvery { produceInitialDataUseCase(userWalletId) } returns Unit.right()
|
||||
coEvery { repository.getCustomerInfo(userWalletId) } returns
|
||||
buildCustomerInfo(kycStatus = KycStatus.PENDING).right()
|
||||
|
||||
// Act
|
||||
val model = createModel(TangemPayOnboardingComponent.Params.HotWalletOnboarding(userWalletId))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 0) { repository.createOrder(any()) }
|
||||
verify(exactly = 1) {
|
||||
router.replaceAll(AppRoute.Wallet, AppRoute.Kyc(userWalletId = userWalletId), onComplete = any())
|
||||
}
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
private fun buildCustomerInfo(kycStatus: KycStatus) = CustomerInfo(
|
||||
customerId = "cust_1",
|
||||
productInstances = emptyList(),
|
||||
cards = emptyList(),
|
||||
kycStatus = kycStatus,
|
||||
state = CustomerInfo.State.NEW,
|
||||
fiatBalance = null,
|
||||
cryptoBalance = null,
|
||||
availableForWithdrawal = BigDecimal.ZERO,
|
||||
tariffPlan = null,
|
||||
)
|
||||
|
||||
private fun TestScope.createModel(params: TangemPayOnboardingComponent.Params): TangemPayOnboardingModel {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TangemPayOnboardingModel(
|
||||
|
|
@ -183,6 +247,7 @@ internal class TangemPayOnboardingModelTest {
|
|||
produceInitialDataUseCase = produceInitialDataUseCase,
|
||||
urlOpener = urlOpener,
|
||||
eligibilityManager = eligibilityManager,
|
||||
tangemPayFeatureToggles = tangemPayFeatureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.domain.models.account.AccountId
|
|||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.pay.TangemPayDetailsInitialRoute
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -159,7 +160,11 @@ internal class DefaultWalletRouter @Inject constructor(
|
|||
}
|
||||
|
||||
override fun openTangemPaySelectPlan(status: AccountStatus.Payment) {
|
||||
// TODO v_rodionov: [REDACTED_TASK_KEY] Tiers Onboarding - part 2
|
||||
val route = AppRoute.TangemPayDetails(
|
||||
status = status,
|
||||
initialRoute = TangemPayDetailsInitialRoute.SELECT_PLAN,
|
||||
)
|
||||
router.push(route)
|
||||
}
|
||||
|
||||
override fun openYieldSupplyBottomSheet(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue