Updated on 2026-08-14
This commit is contained in:
parent
0bd23be25f
commit
2c754243f5
16 changed files with 378 additions and 29 deletions
|
|
@ -298,11 +298,13 @@ internal interface TangemPayDataModule {
|
||||||
fun provideCreateVirtualAccountOrderUseCase(
|
fun provideCreateVirtualAccountOrderUseCase(
|
||||||
onboardingRepository: OnboardingRepository,
|
onboardingRepository: OnboardingRepository,
|
||||||
pollingUseCase: StartTangemPayOrderPollingUseCase,
|
pollingUseCase: StartTangemPayOrderPollingUseCase,
|
||||||
|
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||||
appCoroutineScope: AppCoroutineScope,
|
appCoroutineScope: AppCoroutineScope,
|
||||||
): CreateVirtualAccountOrderUseCase {
|
): CreateVirtualAccountOrderUseCase {
|
||||||
return CreateVirtualAccountOrderUseCase(
|
return CreateVirtualAccountOrderUseCase(
|
||||||
onboardingRepository = onboardingRepository,
|
onboardingRepository = onboardingRepository,
|
||||||
pollingUseCase = pollingUseCase,
|
pollingUseCase = pollingUseCase,
|
||||||
|
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||||
appCoroutineScope = appCoroutineScope,
|
appCoroutineScope = appCoroutineScope,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,10 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
||||||
logger.i("invoke() end ${params.userWalletId}: isRight=${result.isRight()}")
|
logger.i("invoke() end ${params.userWalletId}: isRight=${result.isRight()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun markVirtualAccountProcessing(userWalletId: UserWalletId) {
|
||||||
|
paymentAccountStatusesStore.markVirtualAccountProcessing(userWalletId)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun proceedHasTangemPayResult(
|
private suspend fun proceedHasTangemPayResult(
|
||||||
account: Account.Payment,
|
account: Account.Payment,
|
||||||
hasTangemPay: Boolean,
|
hasTangemPay: Boolean,
|
||||||
|
|
@ -394,8 +398,15 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the Virtual Account on-ramp dimension (VA MVP0, TWI-1638). Gated by the feature toggle.
|
* Resolves the Virtual Account on-ramp dimension (VA MVP0, TWI-1638). Gated by the feature toggle.
|
||||||
* If a product instance with [SpecificationDataType.ACCOUNT] exists, eagerly fetches its bank credentials
|
*
|
||||||
* ([VirtualAccountOnramp.Available]); otherwise surfaces [VirtualAccountOnramp.Eligible] when the wallet has
|
* Resolution order:
|
||||||
|
* 1. A product instance with [SpecificationDataType.ACCOUNT] exists — clears any stale persisted VA order id
|
||||||
|
* (idempotent) and eagerly fetches its bank credentials ([VirtualAccountOnramp.Available], or
|
||||||
|
* [VirtualAccountOnramp.BankCredentialsError] on failure).
|
||||||
|
* 2. Otherwise, a VA order id is persisted locally — checks its status via `getOrderData`:
|
||||||
|
* NEW/PROCESSING/COMPLETED (or a lookup failure) surface [VirtualAccountOnramp.Processing]; CANCELED
|
||||||
|
* clears the persisted id and falls through to eligibility.
|
||||||
|
* 3. Otherwise (or after a CANCELED order) — surfaces [VirtualAccountOnramp.Eligible] when the wallet has
|
||||||
* the `VISA_VIRTUAL_ACCOUNT` eligibility channel (fetched fresh via the user token), else `null`.
|
* the `VISA_VIRTUAL_ACCOUNT` eligibility channel (fetched fresh via the user token), else `null`.
|
||||||
*/
|
*/
|
||||||
private suspend fun CustomerInfo.resolveVirtualAccountOnramp(userWalletId: UserWalletId): VirtualAccountOnramp? {
|
private suspend fun CustomerInfo.resolveVirtualAccountOnramp(userWalletId: UserWalletId): VirtualAccountOnramp? {
|
||||||
|
|
@ -405,6 +416,8 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
||||||
it.specificationDataType == SpecificationDataType.ACCOUNT
|
it.specificationDataType == SpecificationDataType.ACCOUNT
|
||||||
}
|
}
|
||||||
if (accountInstance != null) {
|
if (accountInstance != null) {
|
||||||
|
// Order provisioned into an ACCOUNT product instance — drop the in-flight order hint (idempotent).
|
||||||
|
onboardingRepository.clearVirtualAccountOrderId(userWalletId)
|
||||||
return onboardingRepository.getBankCredentials(userWalletId, accountInstance.id).fold(
|
return onboardingRepository.getBankCredentials(userWalletId, accountInstance.id).fold(
|
||||||
ifLeft = { error ->
|
ifLeft = { error ->
|
||||||
logger.e("getBankCredentials failed for ${accountInstance.id}: $error")
|
logger.e("getBankCredentials failed for ${accountInstance.id}: $error")
|
||||||
|
|
@ -419,6 +432,32 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val vaOrderId = onboardingRepository.getVirtualAccountOrderId(userWalletId)
|
||||||
|
if (vaOrderId != null) {
|
||||||
|
return customerOrderRepository.getOrderData(userWalletId = userWalletId, orderId = vaOrderId).fold(
|
||||||
|
ifLeft = { error ->
|
||||||
|
logger.e("getOrderData(va) failed for $vaOrderId: $error")
|
||||||
|
VirtualAccountOnramp.Processing
|
||||||
|
},
|
||||||
|
ifRight = { orderData ->
|
||||||
|
when (orderData.status) {
|
||||||
|
OrderStatus.CANCELED -> {
|
||||||
|
onboardingRepository.clearVirtualAccountOrderId(userWalletId)
|
||||||
|
resolveEligibility(userWalletId)
|
||||||
|
}
|
||||||
|
OrderStatus.NEW,
|
||||||
|
OrderStatus.PROCESSING,
|
||||||
|
OrderStatus.COMPLETED,
|
||||||
|
-> VirtualAccountOnramp.Processing
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveEligibility(userWalletId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun resolveEligibility(userWalletId: UserWalletId): VirtualAccountOnramp? {
|
||||||
return onboardingRepository.fetchCustomerEligibility(userWalletId).fold(
|
return onboardingRepository.fetchCustomerEligibility(userWalletId).fold(
|
||||||
ifLeft = { error ->
|
ifLeft = { error ->
|
||||||
logger.e("fetchCustomerEligibility failed for $userWalletId: $error")
|
logger.e("fetchCustomerEligibility failed for $userWalletId: $error")
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,13 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun clearVirtualAccountOrderId(userWalletId: UserWalletId) {
|
||||||
|
withContext(dispatcherProvider.io) {
|
||||||
|
val customerWalletAddress = requestHelper.getCustomerWalletAddress(userWalletId)
|
||||||
|
tangemPayStorage.clearVirtualAccountOrderId(customerWalletAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getUserWallet(userWalletId: UserWalletId): UserWallet {
|
private fun getUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||||
return userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId }
|
return userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId }
|
||||||
?: error("no userWallet found")
|
?: error("no userWallet found")
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.models.StatusSource
|
||||||
import com.tangem.domain.models.account.Account
|
import com.tangem.domain.models.account.Account
|
||||||
import com.tangem.domain.models.account.AccountStatus
|
import com.tangem.domain.models.account.AccountStatus
|
||||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||||
|
import com.tangem.domain.models.account.VirtualAccountOnramp
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||||
import com.tangem.utils.coroutines.runSuspendCatching
|
import com.tangem.utils.coroutines.runSuspendCatching
|
||||||
|
|
@ -89,6 +90,28 @@ internal class PaymentAccountStatusesStore(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimistically marks the cached VA on-ramp as [VirtualAccountOnramp.Processing] so the UI reflects a
|
||||||
|
|
||||||
|
* read-modify-write atomically inside [RuntimeSharedStore.update] to avoid a lost update racing with a
|
||||||
|
* concurrent [store]/[updateStatusSource] call. No-op (no write) when there is no cached entry for
|
||||||
|
* [userWalletId], or when its value isn't [PaymentAccountStatusValue.Loaded]. Not persisted, mirroring
|
||||||
|
* [updateStatusSource].
|
||||||
|
*/
|
||||||
|
suspend fun markVirtualAccountProcessing(userWalletId: UserWalletId) {
|
||||||
|
logger.i("markVirtualAccountProcessing($userWalletId)")
|
||||||
|
runtimeStore.update(emptyMap()) { stored ->
|
||||||
|
stored.toMutableMap().apply {
|
||||||
|
val paymentAccountStatus = this[userWalletId.stringValue] ?: return@update stored
|
||||||
|
val loaded = paymentAccountStatus.value as? PaymentAccountStatusValue.Loaded ?: return@update stored
|
||||||
|
val newValue = paymentAccountStatus.copy(
|
||||||
|
value = loaded.copy(virtualAccount = VirtualAccountOnramp.Processing),
|
||||||
|
)
|
||||||
|
put(key = userWalletId.stringValue, value = newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun store(userWalletId: UserWalletId, status: AccountStatus.Payment) {
|
suspend fun store(userWalletId: UserWalletId, status: AccountStatus.Payment) {
|
||||||
logger.i("store($userWalletId): valueType=${status.value::class.simpleName}")
|
logger.i("store($userWalletId): valueType=${status.value::class.simpleName}")
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,14 @@ internal class MockAwareOnboardingRepository @Inject constructor(
|
||||||
real.storeVirtualAccountOrderId(userWalletId, vaOrderId)
|
real.storeVirtualAccountOrderId(userWalletId, vaOrderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun clearVirtualAccountOrderId(userWalletId: UserWalletId) {
|
||||||
|
if (isMockMode) {
|
||||||
|
mockVaOrderIds.remove(userWalletId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
real.clearVirtualAccountOrderId(userWalletId)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> =
|
override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> =
|
||||||
real.hasTangemPayInWallet(userWalletId)
|
real.hasTangemPayInWallet(userWalletId)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,15 @@ package com.tangem.data.pay.flow
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import arrow.core.left
|
import arrow.core.left
|
||||||
|
import arrow.core.right
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter
|
||||||
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
||||||
|
import com.tangem.data.pay.store.WalletIdWithPaymentStatus
|
||||||
|
import com.tangem.data.pay.store.WalletIdWithPaymentStatusDM
|
||||||
|
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||||
|
import com.tangem.domain.models.StatusSource
|
||||||
|
import com.tangem.domain.models.account.Account
|
||||||
import com.tangem.domain.models.account.AccountStatus
|
import com.tangem.domain.models.account.AccountStatus
|
||||||
import com.tangem.domain.models.account.BankCredentials
|
import com.tangem.domain.models.account.BankCredentials
|
||||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||||
|
|
@ -18,11 +25,15 @@ import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||||
import com.tangem.domain.pay.model.CustomerInfo
|
import com.tangem.domain.pay.model.CustomerInfo
|
||||||
|
import com.tangem.domain.pay.model.OrderData
|
||||||
|
import com.tangem.domain.pay.model.OrderStatus
|
||||||
import com.tangem.domain.pay.repository.*
|
import com.tangem.domain.pay.repository.*
|
||||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||||
import com.tangem.domain.visa.error.VisaApiError
|
import com.tangem.domain.visa.error.VisaApiError
|
||||||
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||||
import com.tangem.security.DeviceSecurityInfoProvider
|
import com.tangem.security.DeviceSecurityInfoProvider
|
||||||
|
import com.tangem.test.core.TestAppCoroutineScope
|
||||||
|
import com.tangem.test.core.datastore.MockStateDataStore
|
||||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -107,9 +118,8 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
||||||
isPinSet = true,
|
isPinSet = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun buildCustomerInfo(
|
private fun buildCustomerInfo(productInstances: List<CustomerInfo.ProductInstance> = listOf(cardProductInstance)) =
|
||||||
productInstances: List<CustomerInfo.ProductInstance> = listOf(cardProductInstance),
|
CustomerInfo(
|
||||||
) = CustomerInfo(
|
|
||||||
customerId = "cust_1",
|
customerId = "cust_1",
|
||||||
kycStatus = KycStatus.APPROVED,
|
kycStatus = KycStatus.APPROVED,
|
||||||
state = CustomerInfo.State.ACTIVE,
|
state = CustomerInfo.State.ACTIVE,
|
||||||
|
|
@ -187,7 +197,38 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
||||||
.map { it.value }
|
.map { it.value }
|
||||||
.filterIsInstance<PaymentAccountStatusValue.Loaded>()
|
.filterIsInstance<PaymentAccountStatusValue.Loaded>()
|
||||||
.lastOrNull()
|
.lastOrNull()
|
||||||
return requireNotNull(loaded) { "Expected at least one Loaded status to be stored; stored: ${map { it.value::class.simpleName }}" }
|
return requireNotNull(
|
||||||
|
loaded,
|
||||||
|
) { "Expected at least one Loaded status to be stored; stored: ${map { it.value::class.simpleName }}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds a [PaymentAccountStatusValue.Loaded] fixture with every field defaulted except [virtualAccount]. */
|
||||||
|
private fun loadedFixture(virtualAccount: VirtualAccountOnramp? = null): PaymentAccountStatusValue.Loaded {
|
||||||
|
val token: CryptoCurrency.Token = mockk(relaxed = true)
|
||||||
|
return PaymentAccountStatusValue.Loaded(
|
||||||
|
source = StatusSource.ACTUAL,
|
||||||
|
customerId = "cust_1",
|
||||||
|
depositAddress = "0xdeposit",
|
||||||
|
balance = PaymentAccountStatusValue.Balance(
|
||||||
|
fiatBalance = PaymentAccountStatusValue.FiatBalance(
|
||||||
|
availableBalance = BigDecimal.TEN,
|
||||||
|
currency = "USD",
|
||||||
|
),
|
||||||
|
cryptoBalance = PaymentAccountStatusValue.CryptoBalance(
|
||||||
|
id = "usdc",
|
||||||
|
chainId = 137L,
|
||||||
|
depositAddress = "0xdeposit",
|
||||||
|
tokenContractAddress = "0xcontract",
|
||||||
|
balance = BigDecimal.TEN,
|
||||||
|
),
|
||||||
|
availableForWithdrawal = BigDecimal.TEN,
|
||||||
|
),
|
||||||
|
cryptoCurrency = token,
|
||||||
|
cards = emptyList(),
|
||||||
|
fiatRate = null,
|
||||||
|
error = null,
|
||||||
|
virtualAccount = virtualAccount,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
|
@ -219,6 +260,7 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
||||||
)
|
)
|
||||||
stubHappyPath(customerInfo)
|
stubHappyPath(customerInfo)
|
||||||
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
|
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
|
||||||
|
coEvery { onboardingRepository.clearVirtualAccountOrderId(userWalletId) } just Runs
|
||||||
coEvery {
|
coEvery {
|
||||||
onboardingRepository.getBankCredentials(userWalletId, "pi_account")
|
onboardingRepository.getBankCredentials(userWalletId, "pi_account")
|
||||||
} returns Either.Right(bankCredentialsFixture)
|
} returns Either.Right(bankCredentialsFixture)
|
||||||
|
|
@ -235,6 +277,7 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
||||||
bankCredentials = bankCredentialsFixture,
|
bankCredentials = bankCredentialsFixture,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
coVerify(exactly = 1) { onboardingRepository.clearVirtualAccountOrderId(userWalletId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -246,6 +289,7 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
||||||
)
|
)
|
||||||
stubHappyPath(customerInfo)
|
stubHappyPath(customerInfo)
|
||||||
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
|
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
|
||||||
|
coEvery { onboardingRepository.clearVirtualAccountOrderId(userWalletId) } just Runs
|
||||||
coEvery {
|
coEvery {
|
||||||
onboardingRepository.getBankCredentials(userWalletId, "pi_account")
|
onboardingRepository.getBankCredentials(userWalletId, "pi_account")
|
||||||
} returns VisaApiError.UnknownWithoutCode.left()
|
} returns VisaApiError.UnknownWithoutCode.left()
|
||||||
|
|
@ -257,6 +301,7 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
||||||
// Assert
|
// Assert
|
||||||
val loaded = storedStatuses.lastLoaded()
|
val loaded = storedStatuses.lastLoaded()
|
||||||
assertThat(loaded.virtualAccount).isEqualTo(VirtualAccountOnramp.BankCredentialsError)
|
assertThat(loaded.virtualAccount).isEqualTo(VirtualAccountOnramp.BankCredentialsError)
|
||||||
|
coVerify(exactly = 1) { onboardingRepository.clearVirtualAccountOrderId(userWalletId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -266,6 +311,7 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
||||||
val customerInfo = buildCustomerInfo(productInstances = listOf(cardProductInstance))
|
val customerInfo = buildCustomerInfo(productInstances = listOf(cardProductInstance))
|
||||||
stubHappyPath(customerInfo)
|
stubHappyPath(customerInfo)
|
||||||
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
|
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
|
||||||
|
coEvery { onboardingRepository.getVirtualAccountOrderId(userWalletId) } returns null
|
||||||
coEvery {
|
coEvery {
|
||||||
onboardingRepository.fetchCustomerEligibility(userWalletId)
|
onboardingRepository.fetchCustomerEligibility(userWalletId)
|
||||||
} returns Either.Right(listOf(TangemPayEligibilityType.VISA_VIRTUAL_ACCOUNT))
|
} returns Either.Right(listOf(TangemPayEligibilityType.VISA_VIRTUAL_ACCOUNT))
|
||||||
|
|
@ -286,6 +332,7 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
||||||
val customerInfo = buildCustomerInfo(productInstances = listOf(cardProductInstance))
|
val customerInfo = buildCustomerInfo(productInstances = listOf(cardProductInstance))
|
||||||
stubHappyPath(customerInfo)
|
stubHappyPath(customerInfo)
|
||||||
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
|
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
|
||||||
|
coEvery { onboardingRepository.getVirtualAccountOrderId(userWalletId) } returns null
|
||||||
coEvery {
|
coEvery {
|
||||||
onboardingRepository.fetchCustomerEligibility(userWalletId)
|
onboardingRepository.fetchCustomerEligibility(userWalletId)
|
||||||
} returns VisaApiError.UnknownWithoutCode.left()
|
} returns VisaApiError.UnknownWithoutCode.left()
|
||||||
|
|
@ -298,5 +345,165 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
||||||
val loaded = storedStatuses.lastLoaded()
|
val loaded = storedStatuses.lastLoaded()
|
||||||
assertThat(loaded.virtualAccount).isNull()
|
assertThat(loaded.virtualAccount).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN no instance and va order PROCESSING WHEN invoke THEN virtualAccount is Processing`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val customerInfo = buildCustomerInfo(productInstances = listOf(cardProductInstance))
|
||||||
|
stubHappyPath(customerInfo)
|
||||||
|
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
|
||||||
|
coEvery { onboardingRepository.getVirtualAccountOrderId(userWalletId) } returns "va-1"
|
||||||
|
coEvery {
|
||||||
|
customerOrderRepository.getOrderData(userWalletId, "va-1")
|
||||||
|
} returns OrderData(customerId = "c1", status = OrderStatus.PROCESSING, withdrawTxHash = null).right()
|
||||||
|
val storedStatuses = captureStoredStatuses()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
fetcher.invoke(params)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(storedStatuses.lastLoaded().virtualAccount).isEqualTo(VirtualAccountOnramp.Processing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN no instance and va order COMPLETED but instance absent WHEN invoke THEN virtualAccount is Processing`() =
|
||||||
|
runTest {
|
||||||
|
// Arrange
|
||||||
|
val customerInfo = buildCustomerInfo(productInstances = listOf(cardProductInstance))
|
||||||
|
stubHappyPath(customerInfo)
|
||||||
|
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
|
||||||
|
coEvery { onboardingRepository.getVirtualAccountOrderId(userWalletId) } returns "va-1"
|
||||||
|
coEvery {
|
||||||
|
customerOrderRepository.getOrderData(userWalletId, "va-1")
|
||||||
|
} returns OrderData(customerId = "c1", status = OrderStatus.COMPLETED, withdrawTxHash = null).right()
|
||||||
|
val storedStatuses = captureStoredStatuses()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
fetcher.invoke(params)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(storedStatuses.lastLoaded().virtualAccount).isEqualTo(VirtualAccountOnramp.Processing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN no instance and va getOrderData fails WHEN invoke THEN virtualAccount is Processing`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val customerInfo = buildCustomerInfo(productInstances = listOf(cardProductInstance))
|
||||||
|
stubHappyPath(customerInfo)
|
||||||
|
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
|
||||||
|
coEvery { onboardingRepository.getVirtualAccountOrderId(userWalletId) } returns "va-1"
|
||||||
|
coEvery {
|
||||||
|
customerOrderRepository.getOrderData(userWalletId, "va-1")
|
||||||
|
} returns VisaApiError.UnknownWithoutCode.left()
|
||||||
|
val storedStatuses = captureStoredStatuses()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
fetcher.invoke(params)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(storedStatuses.lastLoaded().virtualAccount).isEqualTo(VirtualAccountOnramp.Processing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN no instance and va order CANCELED WHEN invoke THEN id cleared and falls back to eligibility`() =
|
||||||
|
runTest {
|
||||||
|
// Arrange
|
||||||
|
val customerInfo = buildCustomerInfo(productInstances = listOf(cardProductInstance))
|
||||||
|
stubHappyPath(customerInfo)
|
||||||
|
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
|
||||||
|
coEvery { onboardingRepository.getVirtualAccountOrderId(userWalletId) } returns "va-1"
|
||||||
|
coEvery {
|
||||||
|
customerOrderRepository.getOrderData(userWalletId, "va-1")
|
||||||
|
} returns OrderData(customerId = "c1", status = OrderStatus.CANCELED, withdrawTxHash = null).right()
|
||||||
|
coEvery { onboardingRepository.clearVirtualAccountOrderId(userWalletId) } just Runs
|
||||||
|
coEvery {
|
||||||
|
onboardingRepository.fetchCustomerEligibility(userWalletId)
|
||||||
|
} returns Either.Right(listOf(TangemPayEligibilityType.VISA_VIRTUAL_ACCOUNT))
|
||||||
|
val storedStatuses = captureStoredStatuses()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
fetcher.invoke(params)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 1) { onboardingRepository.clearVirtualAccountOrderId(userWalletId) }
|
||||||
|
assertThat(storedStatuses.lastLoaded().virtualAccount).isEqualTo(VirtualAccountOnramp.Eligible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [markVirtualAccountProcessing] now delegates entirely to the atomic
|
||||||
|
* [PaymentAccountStatusesStore.markVirtualAccountProcessing] (read-modify-write happens inside the store's
|
||||||
|
* `runtimeStore.update` lambda, see [REDACTED_TASK_KEY] review). A mocked store can't exercise that internal branching,
|
||||||
|
* so these tests wire the fetcher to a real [PaymentAccountStatusesStore] (real [RuntimeSharedStore] +
|
||||||
|
* in-memory persistence fake) and assert on its resulting state — exercising the delegate wiring and the
|
||||||
|
* store's atomic logic together.
|
||||||
|
*/
|
||||||
|
@Nested
|
||||||
|
inner class MarkVirtualAccountProcessing {
|
||||||
|
|
||||||
|
private val runtimeStore = RuntimeSharedStore<WalletIdWithPaymentStatus>()
|
||||||
|
private val persistenceStore = MockStateDataStore<WalletIdWithPaymentStatusDM>(default = emptyMap())
|
||||||
|
private val converter: PaymentAccountStatusValueDMConverter = mockk(relaxed = true)
|
||||||
|
|
||||||
|
private val realStore = PaymentAccountStatusesStore(
|
||||||
|
runtimeStore = runtimeStore,
|
||||||
|
persistenceDataStore = persistenceStore,
|
||||||
|
converter = converter,
|
||||||
|
scope = TestAppCoroutineScope(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val realFetcher = DefaultPaymentAccountStatusFetcher(
|
||||||
|
paymentAccountStatusesStore = realStore,
|
||||||
|
onboardingRepository = onboardingRepository,
|
||||||
|
customerOrderRepository = customerOrderRepository,
|
||||||
|
deviceSecurity = deviceSecurity,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
tangemPayCurrencyFactory = tangemPayCurrencyFactory,
|
||||||
|
eligibilityManager = eligibilityManager,
|
||||||
|
reissueCardRepository = reissueCardRepository,
|
||||||
|
singleQuoteSupplier = singleQuoteSupplier,
|
||||||
|
closeCardRepository = closeCardRepository,
|
||||||
|
cardDetailsRepository = cardDetailsRepository,
|
||||||
|
issueCardRepository = issueCardRepository,
|
||||||
|
virtualAccountFeatureToggles = virtualAccountFeatureToggles,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val account = Account.Payment(userWalletId = userWalletId)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN cached Loaded with eligible onramp WHEN mark THEN virtualAccount becomes Processing`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val loaded = loadedFixture(virtualAccount = VirtualAccountOnramp.Eligible)
|
||||||
|
realStore.store(userWalletId, AccountStatus.Payment(account = account, value = loaded))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
realFetcher.markVirtualAccountProcessing(userWalletId)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val updated = realStore.getSyncOrNull(userWalletId)?.value
|
||||||
|
assertThat(updated).isEqualTo(loaded.copy(virtualAccount = VirtualAccountOnramp.Processing))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN no cached value WHEN mark THEN store stays empty`() = runTest {
|
||||||
|
// Act
|
||||||
|
realFetcher.markVirtualAccountProcessing(userWalletId)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(realStore.getSyncOrNull(userWalletId)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN cached non-Loaded value WHEN mark THEN value stays unchanged`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val issuingCard = PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||||
|
realStore.store(userWalletId, AccountStatus.Payment(account = account, value = issuingCard))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
realFetcher.markVirtualAccountProcessing(userWalletId)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(realStore.getSyncOrNull(userWalletId)?.value).isEqualTo(issuingCard)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -22,6 +22,15 @@ sealed interface VirtualAccountOnramp {
|
||||||
val bankCredentials: BankCredentials,
|
val bankCredentials: BankCredentials,
|
||||||
) : VirtualAccountOnramp
|
) : VirtualAccountOnramp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A VA on-ramp order has been submitted and is being provisioned (order status NEW/PROCESSING, or
|
||||||
|
* COMPLETED before the ACCOUNT product instance appears). The bank-transfer entry point stays visible;
|
||||||
|
* tapping it shows the "Preparing your banking details" bottom sheet. Transient — never persisted,
|
||||||
|
* re-resolved on the next status fetch, cleared once the ACCOUNT instance appears or the order is canceled.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data object Processing : VirtualAccountOnramp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VA product instance exists, but its bank credentials failed to load. The bank-transfer entry point
|
* VA product instance exists, but its bank credentials failed to load. The bank-transfer entry point
|
||||||
* stays visible; tapping it surfaces a retryable "couldn't load banking details" error instead of the
|
* stays visible; tapping it surfaces a retryable "couldn't load banking details" error instead of the
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package com.tangem.domain.pay.flow
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import com.tangem.domain.core.flow.FlowFetcher
|
import com.tangem.domain.core.flow.FlowFetcher
|
||||||
|
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||||
|
import com.tangem.domain.models.account.VirtualAccountOnramp
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
||||||
interface PaymentAccountStatusFetcher : FlowFetcher<PaymentAccountStatusFetcher.Params> {
|
interface PaymentAccountStatusFetcher : FlowFetcher<PaymentAccountStatusFetcher.Params> {
|
||||||
|
|
@ -10,5 +12,12 @@ interface PaymentAccountStatusFetcher : FlowFetcher<PaymentAccountStatusFetcher.
|
||||||
return invoke(Params(userWalletId))
|
return invoke(Params(userWalletId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimistically marks the cached VA on-ramp as [VirtualAccountOnramp.Processing] so the UI reflects a
|
||||||
|
|
||||||
|
* cached [PaymentAccountStatusValue.Loaded].
|
||||||
|
*/
|
||||||
|
suspend fun markVirtualAccountProcessing(userWalletId: UserWalletId)
|
||||||
|
|
||||||
data class Params(val userWalletId: UserWalletId)
|
data class Params(val userWalletId: UserWalletId)
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +42,8 @@ interface OnboardingRepository {
|
||||||
|
|
||||||
suspend fun storeVirtualAccountOrderId(userWalletId: UserWalletId, vaOrderId: String)
|
suspend fun storeVirtualAccountOrderId(userWalletId: UserWalletId, vaOrderId: String)
|
||||||
|
|
||||||
|
suspend fun clearVirtualAccountOrderId(userWalletId: UserWalletId)
|
||||||
|
|
||||||
suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean>
|
suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean>
|
||||||
|
|
||||||
suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType>
|
suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tangem.domain.pay.usecase
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import arrow.core.raise.either
|
import arrow.core.raise.either
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||||
import com.tangem.domain.pay.model.OrderStatus
|
import com.tangem.domain.pay.model.OrderStatus
|
||||||
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
||||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||||
|
|
@ -22,6 +23,7 @@ import java.util.UUID
|
||||||
class CreateVirtualAccountOrderUseCase(
|
class CreateVirtualAccountOrderUseCase(
|
||||||
private val onboardingRepository: OnboardingRepository,
|
private val onboardingRepository: OnboardingRepository,
|
||||||
private val pollingUseCase: StartTangemPayOrderPollingUseCase,
|
private val pollingUseCase: StartTangemPayOrderPollingUseCase,
|
||||||
|
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||||
private val appCoroutineScope: AppCoroutineScope,
|
private val appCoroutineScope: AppCoroutineScope,
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
|
|
@ -36,6 +38,9 @@ class CreateVirtualAccountOrderUseCase(
|
||||||
idempotencyKey = UUID.randomUUID().toString(),
|
idempotencyKey = UUID.randomUUID().toString(),
|
||||||
).bind()
|
).bind()
|
||||||
onboardingRepository.storeVirtualAccountOrderId(userWalletId = userWalletId, vaOrderId = vaOrderId)
|
onboardingRepository.storeVirtualAccountOrderId(userWalletId = userWalletId, vaOrderId = vaOrderId)
|
||||||
|
// Optimistically flip the cached on-ramp to Processing so the UI shows "Preparing" immediately
|
||||||
|
// (no wait for the poll/refetch to confirm).
|
||||||
|
paymentAccountStatusFetcher.markVirtualAccountProcessing(userWalletId)
|
||||||
appCoroutineScope.launch {
|
appCoroutineScope.launch {
|
||||||
pollingUseCase.invoke(
|
pollingUseCase.invoke(
|
||||||
order = TangemPayOrderInfo(orderId = vaOrderId, orderStatus = OrderStatus.NEW),
|
order = TangemPayOrderInfo(orderId = vaOrderId, orderStatus = OrderStatus.NEW),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import arrow.core.left
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||||
import com.tangem.domain.visa.error.VisaApiError
|
import com.tangem.domain.visa.error.VisaApiError
|
||||||
import com.tangem.test.core.TestAppCoroutineScope
|
import com.tangem.test.core.TestAppCoroutineScope
|
||||||
|
|
@ -17,9 +18,12 @@ internal class CreateVirtualAccountOrderUseCaseTest {
|
||||||
|
|
||||||
private val onboardingRepository: OnboardingRepository = mockk(relaxUnitFun = true)
|
private val onboardingRepository: OnboardingRepository = mockk(relaxUnitFun = true)
|
||||||
private val pollingUseCase: StartTangemPayOrderPollingUseCase = mockk(relaxed = true)
|
private val pollingUseCase: StartTangemPayOrderPollingUseCase = mockk(relaxed = true)
|
||||||
|
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk(relaxUnitFun = true)
|
||||||
|
|
||||||
private val useCase = CreateVirtualAccountOrderUseCase(
|
private val useCase = CreateVirtualAccountOrderUseCase(
|
||||||
onboardingRepository = onboardingRepository,
|
onboardingRepository = onboardingRepository,
|
||||||
pollingUseCase = pollingUseCase,
|
pollingUseCase = pollingUseCase,
|
||||||
|
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||||
appCoroutineScope = TestAppCoroutineScope(),
|
appCoroutineScope = TestAppCoroutineScope(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -36,6 +40,7 @@ internal class CreateVirtualAccountOrderUseCaseTest {
|
||||||
coVerify(exactly = 0) { onboardingRepository.createVirtualAccountOrder(any(), any(), any()) }
|
coVerify(exactly = 0) { onboardingRepository.createVirtualAccountOrder(any(), any(), any()) }
|
||||||
coVerify(exactly = 0) { onboardingRepository.storeVirtualAccountOrderId(any(), any()) }
|
coVerify(exactly = 0) { onboardingRepository.storeVirtualAccountOrderId(any(), any()) }
|
||||||
coVerify(exactly = 0) { pollingUseCase.invoke(any(), any()) }
|
coVerify(exactly = 0) { pollingUseCase.invoke(any(), any()) }
|
||||||
|
coVerify(exactly = 0) { paymentAccountStatusFetcher.markVirtualAccountProcessing(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -50,6 +55,7 @@ internal class CreateVirtualAccountOrderUseCaseTest {
|
||||||
assertThat(result.isRight()).isTrue()
|
assertThat(result.isRight()).isTrue()
|
||||||
coVerify(exactly = 1) { onboardingRepository.storeVirtualAccountOrderId(userWalletId, "new-id") }
|
coVerify(exactly = 1) { onboardingRepository.storeVirtualAccountOrderId(userWalletId, "new-id") }
|
||||||
coVerify(exactly = 1) { pollingUseCase.invoke(any(), userWalletId) }
|
coVerify(exactly = 1) { pollingUseCase.invoke(any(), userWalletId) }
|
||||||
|
coVerify(exactly = 1) { paymentAccountStatusFetcher.markVirtualAccountProcessing(userWalletId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -64,5 +70,6 @@ internal class CreateVirtualAccountOrderUseCaseTest {
|
||||||
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
||||||
coVerify(exactly = 0) { onboardingRepository.storeVirtualAccountOrderId(any(), any()) }
|
coVerify(exactly = 0) { onboardingRepository.storeVirtualAccountOrderId(any(), any()) }
|
||||||
coVerify(exactly = 0) { pollingUseCase.invoke(any(), any()) }
|
coVerify(exactly = 0) { pollingUseCase.invoke(any(), any()) }
|
||||||
|
coVerify(exactly = 0) { paymentAccountStatusFetcher.markVirtualAccountProcessing(any()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -496,6 +496,7 @@ internal class TangemPayCardPageModel @Inject constructor(
|
||||||
when (val onramp = loaded.virtualAccount) {
|
when (val onramp = loaded.virtualAccount) {
|
||||||
null -> return
|
null -> return
|
||||||
is VirtualAccountOnramp.BankCredentialsError -> showVaBankingDetailsError()
|
is VirtualAccountOnramp.BankCredentialsError -> showVaBankingDetailsError()
|
||||||
|
VirtualAccountOnramp.Processing -> showVaPreparing()
|
||||||
is VirtualAccountOnramp.Available,
|
is VirtualAccountOnramp.Available,
|
||||||
VirtualAccountOnramp.Eligible,
|
VirtualAccountOnramp.Eligible,
|
||||||
-> openVirtualAccountDeposit(onramp, loaded)
|
-> openVirtualAccountDeposit(onramp, loaded)
|
||||||
|
|
@ -521,6 +522,11 @@ internal class TangemPayCardPageModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showVaPreparing() {
|
||||||
|
bottomSheetNavigation.dismiss()
|
||||||
|
uiMessageSender.send(message = TangemPayMessagesFactory.createVaPreparingMessage())
|
||||||
|
}
|
||||||
|
|
||||||
fun onVaBankingDetailsResolved(onramp: VirtualAccountOnramp) {
|
fun onVaBankingDetailsResolved(onramp: VirtualAccountOnramp) {
|
||||||
val loaded = currentStatus.value.ifLoadedOrNull { it } ?: return
|
val loaded = currentStatus.value.ifLoadedOrNull { it } ?: return
|
||||||
openVirtualAccountDeposit(onramp, loaded)
|
openVirtualAccountDeposit(onramp, loaded)
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
||||||
when (val onramp = loaded.virtualAccount) {
|
when (val onramp = loaded.virtualAccount) {
|
||||||
null -> return
|
null -> return
|
||||||
is VirtualAccountOnramp.BankCredentialsError -> showVaBankingDetailsError()
|
is VirtualAccountOnramp.BankCredentialsError -> showVaBankingDetailsError()
|
||||||
|
VirtualAccountOnramp.Processing -> showVaPreparing()
|
||||||
is VirtualAccountOnramp.Available,
|
is VirtualAccountOnramp.Available,
|
||||||
VirtualAccountOnramp.Eligible,
|
VirtualAccountOnramp.Eligible,
|
||||||
-> openVirtualAccountDeposit(onramp, loaded)
|
-> openVirtualAccountDeposit(onramp, loaded)
|
||||||
|
|
@ -341,6 +342,11 @@ internal class TangemPayDetailsModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showVaPreparing() {
|
||||||
|
bottomSheetNavigation.dismiss()
|
||||||
|
uiMessageSender.send(message = TangemPayMessagesFactory.createVaPreparingMessage())
|
||||||
|
}
|
||||||
|
|
||||||
fun onVaBankingDetailsResolved(onramp: VirtualAccountOnramp) {
|
fun onVaBankingDetailsResolved(onramp: VirtualAccountOnramp) {
|
||||||
val loaded = currentStatus.value.ifLoadedOrNull { it } ?: return
|
val loaded = currentStatus.value.ifLoadedOrNull { it } ?: return
|
||||||
openVirtualAccountDeposit(onramp, loaded)
|
openVirtualAccountDeposit(onramp, loaded)
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,11 @@ internal class TangemPayVirtualAccountDepositModel @Inject constructor(
|
||||||
analytics.send(TangemPayAnalyticsEvents.VaShowDetailsFirstTimeClicked())
|
analytics.send(TangemPayAnalyticsEvents.VaShowDetailsFirstTimeClicked())
|
||||||
createVirtualAccountOrder()
|
createVirtualAccountOrder()
|
||||||
}
|
}
|
||||||
// Error onramp is intercepted before this sheet opens (a dedicated error sheet is shown instead);
|
// Processing/Error onramps never reach this sheet (a message/error sheet is shown instead);
|
||||||
// the branch only keeps the `when` exhaustive.
|
// these branches only keep the `when` exhaustive.
|
||||||
VirtualAccountOnramp.BankCredentialsError -> onDismiss()
|
VirtualAccountOnramp.Processing,
|
||||||
|
VirtualAccountOnramp.BankCredentialsError,
|
||||||
|
-> onDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,23 @@ internal object TangemPayMessagesFactory {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createVaPreparingMessage(): BottomSheetMessage {
|
||||||
|
return bottomSheetMessage {
|
||||||
|
infoBlock {
|
||||||
|
icon(R.drawable.ic_clock_24) {
|
||||||
|
type = MessageBottomSheetUM.Icon.Type.Informative
|
||||||
|
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.Informative
|
||||||
|
}
|
||||||
|
title = TextReference.Res(R.string.tangempay_bank_transfer_success_title)
|
||||||
|
body = TextReference.Res(R.string.tangempay_bank_transfer_success_subtitle)
|
||||||
|
}
|
||||||
|
secondaryButton {
|
||||||
|
text = resourceReference(R.string.common_got_it)
|
||||||
|
onClick { closeBs() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createFutureFeature(onGotItClick: () -> Unit): BottomSheetMessage {
|
fun createFutureFeature(onGotItClick: () -> Unit): BottomSheetMessage {
|
||||||
return bottomSheetMessage {
|
return bottomSheetMessage {
|
||||||
infoBlock {
|
infoBlock {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ internal fun BankCredentials.toRequisitesRows(): List<RequisitesRow> = listOf(
|
||||||
RequisitesRow(
|
RequisitesRow(
|
||||||
title = resourceReference(R.string.virtual_account_requisites_beneficiary_address),
|
title = resourceReference(R.string.virtual_account_requisites_beneficiary_address),
|
||||||
titleForShare = "Beneficiary address",
|
titleForShare = "Beneficiary address",
|
||||||
value = beneficiaryBankAddress,
|
value = beneficiaryAddress,
|
||||||
),
|
),
|
||||||
RequisitesRow(
|
RequisitesRow(
|
||||||
title = resourceReference(R.string.virtual_account_requisites_bank_name),
|
title = resourceReference(R.string.virtual_account_requisites_bank_name),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue