Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-30 21:18:17 +05:00
parent e2af488b32
commit fdbd09cccb
20 changed files with 852 additions and 77 deletions

View file

@ -5,7 +5,6 @@ import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.CardDisplayName import com.tangem.domain.models.account.CardDisplayName
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.pay.* import com.tangem.domain.models.pay.*
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayCurrencyFactory import com.tangem.domain.pay.TangemPayCurrencyFactory
@ -119,7 +118,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
) )
}, },
error = null, error = null,
virtualAccount = VirtualAccountOnramp.None, virtualAccount = null,
tariffPlan = null, tariffPlan = null,
) )
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview( is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(

View file

@ -403,11 +403,10 @@ 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 * If a product instance with [SpecificationDataType.ACCOUNT] exists, eagerly fetches its bank credentials
* ([VirtualAccountOnramp.Available]); otherwise surfaces [VirtualAccountOnramp.Eligible] when the wallet has * ([VirtualAccountOnramp.Available]); otherwise surfaces [VirtualAccountOnramp.Eligible] when the wallet has
* the `VISA_VIRTUAL_ACCOUNT` eligibility channel (fetched fresh via the user token), else * the `VISA_VIRTUAL_ACCOUNT` eligibility channel (fetched fresh via the user token), else `null`.
* [VirtualAccountOnramp.None].
*/ */
private suspend fun CustomerInfo.resolveVirtualAccountOnramp(userWalletId: UserWalletId): VirtualAccountOnramp { private suspend fun CustomerInfo.resolveVirtualAccountOnramp(userWalletId: UserWalletId): VirtualAccountOnramp? {
if (!virtualAccountFeatureToggles.isVaMvp0Enabled) return VirtualAccountOnramp.None if (!virtualAccountFeatureToggles.isVaMvp0Enabled) return null
val accountInstance = productInstances.firstOrNull { val accountInstance = productInstances.firstOrNull {
it.specificationDataType == SpecificationDataType.ACCOUNT it.specificationDataType == SpecificationDataType.ACCOUNT
@ -416,7 +415,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
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")
VirtualAccountOnramp.None null
}, },
ifRight = { credentials -> ifRight = { credentials ->
VirtualAccountOnramp.Available( VirtualAccountOnramp.Available(
@ -430,13 +429,13 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
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")
VirtualAccountOnramp.None null
}, },
ifRight = { channels -> ifRight = { channels ->
if (channels.contains(TangemPayEligibilityType.VISA_VIRTUAL_ACCOUNT)) { if (channels.contains(TangemPayEligibilityType.VISA_VIRTUAL_ACCOUNT)) {
VirtualAccountOnramp.Eligible VirtualAccountOnramp.Eligible
} else { } else {
VirtualAccountOnramp.None null
} }
}, },
) )

View file

@ -0,0 +1,303 @@
package com.tangem.data.pay.flow
import arrow.core.Either
import arrow.core.left
import com.google.common.truth.Truth.assertThat
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.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.TangemPayEligibilityType
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayCurrencyFactory
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.repository.*
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultPaymentAccountStatusFetcherTest {
private val paymentAccountStatusesStore: PaymentAccountStatusesStore = mockk(relaxed = true)
private val onboardingRepository: OnboardingRepository = mockk()
private val customerOrderRepository: CustomerOrderRepository = mockk()
private val deviceSecurity: DeviceSecurityInfoProvider = mockk(relaxed = true)
private val dispatchers = TestingCoroutineDispatcherProvider()
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory = mockk()
private val eligibilityManager: TangemPayEligibilityManager = mockk(relaxed = true)
private val reissueCardRepository: TangemPayReissueCardRepository = mockk()
private val singleQuoteSupplier: SingleQuoteStatusSupplier = mockk()
private val closeCardRepository: TangemPayCloseCardRepository = mockk()
private val cardDetailsRepository: TangemPayCardDetailsRepository = mockk()
private val issueCardRepository: TangemPayIssueCardRepository = mockk()
private val virtualAccountFeatureToggles: VirtualAccountFeatureToggles = mockk()
private val fetcher = DefaultPaymentAccountStatusFetcher(
paymentAccountStatusesStore = paymentAccountStatusesStore,
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 userWalletId = UserWalletId("011")
private val params = PaymentAccountStatusFetcher.Params(userWalletId)
private val bankCredentialsFixture = BankCredentials(
type = "ACH",
beneficiaryName = "Test Beneficiary",
beneficiaryAddress = "123 Main St",
beneficiaryBankName = "Test Bank",
beneficiaryBankAddress = "456 Bank Ave",
accountNumber = "1234567890",
routingNumber = "021000021",
)
private val cardProductInstance = CustomerInfo.ProductInstance(
id = "pi_card",
cardId = "card_1",
frozenState = TangemPayCardFrozenState.Unfrozen,
displayName = null,
actualCardLimit = null,
adminCardLimit = null,
status = CustomerInfo.ProductInstance.Status.ACTIVE,
specificationDataType = CustomerInfo.ProductInstance.SpecificationDataType.CARD,
)
private val accountProductInstance = CustomerInfo.ProductInstance(
id = "pi_account",
cardId = "",
frozenState = TangemPayCardFrozenState.Unfrozen,
displayName = null,
actualCardLimit = null,
adminCardLimit = null,
status = CustomerInfo.ProductInstance.Status.ACTIVE,
specificationDataType = CustomerInfo.ProductInstance.SpecificationDataType.ACCOUNT,
)
private val cardInfo = CustomerInfo.CardInfo(
cardId = "card_1",
cardStatus = TangemPayCard.Status.ACTIVE,
lastFourDigits = "1234",
isPinSet = true,
)
private fun buildCustomerInfo(
productInstances: List<CustomerInfo.ProductInstance> = listOf(cardProductInstance),
) = CustomerInfo(
customerId = "cust_1",
kycStatus = KycStatus.APPROVED,
state = CustomerInfo.State.ACTIVE,
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,
cards = listOf(cardInfo),
productInstances = productInstances,
tariffPlan = null,
)
@BeforeEach
fun setUp() {
clearMocks(
onboardingRepository,
customerOrderRepository,
tangemPayCurrencyFactory,
reissueCardRepository,
singleQuoteSupplier,
closeCardRepository,
cardDetailsRepository,
issueCardRepository,
virtualAccountFeatureToggles,
)
// Relaxed mocks don't need clearing — deviceSecurity, eligibilityManager, paymentAccountStatusesStore
// are relaxed and consistent with their relaxed defaults (false, empty, etc.)
clearMocks(paymentAccountStatusesStore, answers = false)
}
/**
* Stubs the full happy-path chain up to [CustomerInfo.convertToContentState] so the fetcher
* can produce a [PaymentAccountStatusValue.Loaded] result. Only the [customerInfo] parameter is
* varied per test to exercise different VA on-ramp branches.
*/
private suspend fun stubHappyPath(customerInfo: CustomerInfo) {
val token: CryptoCurrency.Token = mockk(relaxed = true)
coEvery { onboardingRepository.hasTangemPayInWallet(userWalletId) } returns Either.Right(true)
coEvery { onboardingRepository.isTangemPayInitialDataProduced(userWalletId) } returns true
coEvery { onboardingRepository.getOrderId(userWalletId) } returns null
coEvery { onboardingRepository.getCustomerInfo(userWalletId) } returns Either.Right(customerInfo)
coEvery { paymentAccountStatusesStore.getSyncOrNull(userWalletId) } returns null
coEvery { paymentAccountStatusesStore.store(any(), any()) } just Runs
every { tangemPayCurrencyFactory.create(userWalletId) } returns token
coEvery { singleQuoteSupplier.getSyncOrNull(any()) } returns null
coEvery { cardDetailsRepository.cardFrozenStateSync(any()) } returns TangemPayCardFrozenState.Unfrozen
coEvery { closeCardRepository.getCloseOrderId(any(), any()) } returns Either.Right(null)
coEvery { reissueCardRepository.getReissueOrderId(any(), any()) } returns Either.Right(null)
coEvery { issueCardRepository.getIssueOrderIds(any()) } returns emptyList()
}
/** Collects all [AccountStatus.Payment] values stored via [PaymentAccountStatusesStore.store]. */
private fun captureStoredStatuses(): MutableList<AccountStatus.Payment> {
val captured = mutableListOf<AccountStatus.Payment>()
coEvery { paymentAccountStatusesStore.store(any(), capture(captured)) } just Runs
return captured
}
private fun MutableList<AccountStatus.Payment>.lastLoaded(): PaymentAccountStatusValue.Loaded {
val loaded = filterIsInstance<AccountStatus.Payment>()
.map { it.value }
.filterIsInstance<PaymentAccountStatusValue.Loaded>()
.lastOrNull()
return requireNotNull(loaded) { "Expected at least one Loaded status to be stored; stored: ${map { it.value::class.simpleName }}" }
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class `resolveVirtualAccountOnramp` {
@Test
fun `GIVEN feature toggle is off WHEN invoke THEN virtualAccount is null`() = runTest {
// Arrange
val customerInfo = buildCustomerInfo(productInstances = listOf(cardProductInstance))
stubHappyPath(customerInfo)
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns false
val storedStatuses = captureStoredStatuses()
// Act
fetcher.invoke(params)
// Assert
val loaded = storedStatuses.lastLoaded()
assertThat(loaded.virtualAccount).isNull()
}
@Test
fun `GIVEN toggle on and ACCOUNT instance with bank credentials WHEN invoke THEN virtualAccount is Available`() =
runTest {
// Arrange
val customerInfo = buildCustomerInfo(
productInstances = listOf(cardProductInstance, accountProductInstance),
)
stubHappyPath(customerInfo)
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
coEvery {
onboardingRepository.getBankCredentials(userWalletId, "pi_account")
} returns Either.Right(bankCredentialsFixture)
val storedStatuses = captureStoredStatuses()
// Act
fetcher.invoke(params)
// Assert
val loaded = storedStatuses.lastLoaded()
assertThat(loaded.virtualAccount).isEqualTo(
VirtualAccountOnramp.Available(
productInstanceId = "pi_account",
bankCredentials = bankCredentialsFixture,
),
)
}
@Test
fun `GIVEN toggle on and ACCOUNT instance but bank credentials fetch fails WHEN invoke THEN virtualAccount is null`() =
runTest {
// Arrange
val customerInfo = buildCustomerInfo(
productInstances = listOf(cardProductInstance, accountProductInstance),
)
stubHappyPath(customerInfo)
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
coEvery {
onboardingRepository.getBankCredentials(userWalletId, "pi_account")
} returns VisaApiError.UnknownWithoutCode.left()
val storedStatuses = captureStoredStatuses()
// Act
fetcher.invoke(params)
// Assert
val loaded = storedStatuses.lastLoaded()
assertThat(loaded.virtualAccount).isNull()
}
@Test
fun `GIVEN toggle on and no ACCOUNT instance and customer is eligible WHEN invoke THEN virtualAccount is Eligible`() =
runTest {
// Arrange
val customerInfo = buildCustomerInfo(productInstances = listOf(cardProductInstance))
stubHappyPath(customerInfo)
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
coEvery {
onboardingRepository.fetchCustomerEligibility(userWalletId)
} returns Either.Right(listOf(TangemPayEligibilityType.VISA_VIRTUAL_ACCOUNT))
val storedStatuses = captureStoredStatuses()
// Act
fetcher.invoke(params)
// Assert
val loaded = storedStatuses.lastLoaded()
assertThat(loaded.virtualAccount).isEqualTo(VirtualAccountOnramp.Eligible)
}
@Test
fun `GIVEN toggle on and no ACCOUNT instance and eligibility fetch fails WHEN invoke THEN virtualAccount is null`() =
runTest {
// Arrange
val customerInfo = buildCustomerInfo(productInstances = listOf(cardProductInstance))
stubHappyPath(customerInfo)
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns true
coEvery {
onboardingRepository.fetchCustomerEligibility(userWalletId)
} returns VisaApiError.UnknownWithoutCode.left()
val storedStatuses = captureStoredStatuses()
// Act
fetcher.invoke(params)
// Assert
val loaded = storedStatuses.lastLoaded()
assertThat(loaded.virtualAccount).isNull()
}
}
}

View file

@ -149,7 +149,8 @@ sealed class PaymentAccountStatusValue {
* [totalFiatBalance] resolves to [TotalFiatBalance.Failed]. * [totalFiatBalance] resolves to [TotalFiatBalance.Failed].
* @property error Transient error overlaid on top of cached data when a refresh fails * @property error Transient error overlaid on top of cached data when a refresh fails
* (see [copySealed]), or `null` when the status is up to date. Not persisted. * (see [copySealed]), or `null` when the status is up to date. Not persisted.
* @property virtualAccount Virtual Account (Visa on-ramp) availability VA MVP0 (TWI-1638). * @property virtualAccount Virtual Account (Visa on-ramp) availability VA MVP0 (TWI-1638), or `null`
* when not applicable (feature toggle off / wallet not eligible).
* Transient: not persisted in the local cache. * Transient: not persisted in the local cache.
* @property tariffPlan Current tariff plan with subscription data (Tiers). * @property tariffPlan Current tariff plan with subscription data (Tiers).
* Transient: not persisted in the local cache. * Transient: not persisted in the local cache.
@ -164,7 +165,7 @@ sealed class PaymentAccountStatusValue {
val cards: List<TangemPayCard>, val cards: List<TangemPayCard>,
val fiatRate: SerializedBigDecimal?, val fiatRate: SerializedBigDecimal?,
val error: Error?, val error: Error?,
val virtualAccount: VirtualAccountOnramp, val virtualAccount: VirtualAccountOnramp?,
val tariffPlan: TangemPayCustomerTariffPlan?, val tariffPlan: TangemPayCustomerTariffPlan?,
) : PaymentAccountStatusValue() { ) : PaymentAccountStatusValue() {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(

View file

@ -11,10 +11,6 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
sealed interface VirtualAccountOnramp { sealed interface VirtualAccountOnramp {
/** On-ramp not applicable: feature toggle off, or wallet not eligible. */
@Serializable
data object None : VirtualAccountOnramp
/** No VA product instance yet, but the wallet is eligible to add funds (channel `VISA_VIRTUAL_ACCOUNT`). */ /** No VA product instance yet, but the wallet is eligible to add funds (channel `VISA_VIRTUAL_ACCOUNT`). */
@Serializable @Serializable
data object Eligible : VirtualAccountOnramp data object Eligible : VirtualAccountOnramp

View file

@ -32,6 +32,7 @@ dependencies {
implementation(projects.features.txhistory.api) implementation(projects.features.txhistory.api)
implementation(projects.features.tokendetails.api) implementation(projects.features.tokendetails.api)
implementation(projects.features.promoBanners.api) implementation(projects.features.promoBanners.api)
implementation(projects.features.virtualAccounts.details.api) // TWI_1638_VA_MVP0_ENABLED
/** Domain */ /** Domain */
implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding)

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.account.VirtualAccountOnramp
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.TangemPayTopUpData import com.tangem.domain.pay.model.TangemPayTopUpData
@ -39,11 +40,13 @@ internal class TangemPayAddFundsComponent(
val fiatBalance: BigDecimal, val fiatBalance: BigDecimal,
val depositAddress: String, val depositAddress: String,
val cryptoCurrency: CryptoCurrency, val cryptoCurrency: CryptoCurrency,
val virtualAccountOnramp: VirtualAccountOnramp?,
) )
} }
internal interface AddFundsListener { internal interface AddFundsListener {
fun onClickReceive(data: TangemPayTopUpData) fun onClickReceive(data: TangemPayTopUpData)
fun onClickSwap(data: TangemPayTopUpData) fun onClickSwap(data: TangemPayTopUpData)
fun onClickBankTransfer()
fun onDismissAddFunds() fun onDismissAddFunds()
} }

View file

@ -97,6 +97,14 @@ internal class TangemPayCardPageScreenComponent(
fiatBalance = navigation.fiatBalance, fiatBalance = navigation.fiatBalance,
depositAddress = navigation.depositAddress, depositAddress = navigation.depositAddress,
cryptoCurrency = navigation.cryptoCurrency, cryptoCurrency = navigation.cryptoCurrency,
virtualAccountOnramp = navigation.virtualAccountOnramp,
),
)
is TangemPayCardNavigation.VirtualAccountDeposit -> TangemPayVirtualAccountDepositComponent(
appComponentContext = context,
params = TangemPayVirtualAccountDepositComponent.Params(
virtualAccountOnramp = navigation.virtualAccountOnramp,
onDismiss = model.bottomSheetNavigation::dismiss,
), ),
) )
is TangemPayCardNavigation.Receive -> tokenReceiveComponentFactory.create( is TangemPayCardNavigation.Receive -> tokenReceiveComponentFactory.create(

View file

@ -143,6 +143,14 @@ internal class TangemPayDetailsComponent(
depositAddress = navigation.depositAddress, depositAddress = navigation.depositAddress,
cryptoCurrency = navigation.cryptoCurrency, cryptoCurrency = navigation.cryptoCurrency,
listener = model, listener = model,
virtualAccountOnramp = navigation.virtualAccountOnramp,
),
)
is TangemPayDetailsNavigation.VirtualAccountDeposit -> TangemPayVirtualAccountDepositComponent(
appComponentContext = context,
params = TangemPayVirtualAccountDepositComponent.Params(
virtualAccountOnramp = navigation.virtualAccountOnramp,
onDismiss = model.bottomSheetNavigation::dismiss,
), ),
) )
is TangemPayDetailsNavigation.IssueAdditionalCard -> TangemPayIssueAdditionalCardComponent( is TangemPayDetailsNavigation.IssueAdditionalCard -> TangemPayIssueAdditionalCardComponent(

View file

@ -0,0 +1,35 @@
package com.tangem.features.tangempay.components
import androidx.compose.runtime.Composable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.account.VirtualAccountOnramp
import com.tangem.features.tangempay.model.TangemPayVirtualAccountDepositModel
import com.tangem.features.tangempay.ui.TangemPayVirtualAccountDepositBottomSheet
/**
* Bank-transfer deposit bottom sheet (VA MVP0, TWI-1638). Opened from the add-funds "Bank transfer" option.
* Renders the on-ramp intro; the [VirtualAccountOnramp.Eligible] state additionally shows a T&C consent footer.
*/
internal class TangemPayVirtualAccountDepositComponent(
appComponentContext: AppComponentContext,
params: Params,
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
private val model: TangemPayVirtualAccountDepositModel = getOrCreateModel(params = params)
override fun dismiss() {
model.onDismiss()
}
@Composable
override fun BottomSheet() {
TangemPayVirtualAccountDepositBottomSheet(state = model.uiState)
}
data class Params(
val virtualAccountOnramp: VirtualAccountOnramp,
val onDismiss: () -> Unit,
)
}

View file

@ -46,6 +46,11 @@ internal interface TangemPayModelModule {
@ClassKey(TangemPayAddFundsModel::class) @ClassKey(TangemPayAddFundsModel::class)
fun bindTangemPayAddFundsModel(model: TangemPayAddFundsModel): Model fun bindTangemPayAddFundsModel(model: TangemPayAddFundsModel): Model
@Binds
@IntoMap
@ClassKey(TangemPayVirtualAccountDepositModel::class)
fun bindTangemPayVirtualAccountDepositModel(model: TangemPayVirtualAccountDepositModel): Model
@Binds @Binds
@IntoMap @IntoMap
@ClassKey(TangemPayViewPinModel::class) @ClassKey(TangemPayViewPinModel::class)

View file

@ -1,6 +1,7 @@
package com.tangem.features.tangempay.entity package com.tangem.features.tangempay.entity
import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.VirtualAccountOnramp
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.serialization.SerializedBigDecimal import com.tangem.domain.models.serialization.SerializedBigDecimal
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
@ -30,6 +31,12 @@ internal sealed class TangemPayCardNavigation {
val fiatBalance: SerializedBigDecimal, val fiatBalance: SerializedBigDecimal,
val depositAddress: String, val depositAddress: String,
val cryptoCurrency: CryptoCurrency, val cryptoCurrency: CryptoCurrency,
val virtualAccountOnramp: VirtualAccountOnramp?,
) : TangemPayCardNavigation()
@Serializable
data class VirtualAccountDeposit(
val virtualAccountOnramp: VirtualAccountOnramp,
) : TangemPayCardNavigation() ) : TangemPayCardNavigation()
@Serializable @Serializable

View file

@ -1,6 +1,7 @@
package com.tangem.features.tangempay.entity package com.tangem.features.tangempay.entity
import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.VirtualAccountOnramp
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.serialization.SerializedBigDecimal import com.tangem.domain.models.serialization.SerializedBigDecimal
import com.tangem.domain.models.serialization.SerializedCurrency import com.tangem.domain.models.serialization.SerializedCurrency
@ -21,6 +22,12 @@ internal sealed class TangemPayDetailsNavigation {
val fiatBalance: SerializedBigDecimal, val fiatBalance: SerializedBigDecimal,
val depositAddress: String, val depositAddress: String,
val cryptoCurrency: CryptoCurrency, val cryptoCurrency: CryptoCurrency,
val virtualAccountOnramp: VirtualAccountOnramp?,
) : TangemPayDetailsNavigation()
@Serializable
data class VirtualAccountDeposit(
val virtualAccountOnramp: VirtualAccountOnramp,
) : TangemPayDetailsNavigation() ) : TangemPayDetailsNavigation()
@Serializable @Serializable

View file

@ -0,0 +1,27 @@
package com.tangem.features.tangempay.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
/**
* UI state of the bank-transfer deposit bottom sheet (VA MVP0, TWI-1638).
*
* @property shouldShowTermsAndConditions `true` for the `Eligible` state shows the provider T&C consent footer.
*/
@Immutable
internal data class TangemPayVirtualAccountDepositUM(
val fees: ImmutableList<FeeRow>,
val shouldShowTermsAndConditions: Boolean,
val onShowDetailsClick: () -> Unit,
val onDismiss: () -> Unit,
val onTermsClick: () -> Unit,
val onPrivacyClick: () -> Unit,
) {
@Immutable
data class FeeRow(
val title: TextReference,
val value: String,
)
}

View file

@ -11,6 +11,7 @@ import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tangempay.components.TangemPayAddFundsComponent import com.tangem.features.tangempay.components.TangemPayAddFundsComponent
import com.tangem.features.tangempay.entity.TangemPayAddFundsUM import com.tangem.features.tangempay.entity.TangemPayAddFundsUM
import com.tangem.features.tangempay.model.transformers.TangemPayAddFundsUMConverter import com.tangem.features.tangempay.model.transformers.TangemPayAddFundsUMConverter
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import javax.inject.Inject import javax.inject.Inject
@ -20,6 +21,7 @@ internal class TangemPayAddFundsModel @Inject constructor(
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val virtualAccountToggles: VirtualAccountFeatureToggles,
) : Model() { ) : Model() {
private val params = paramsContainer.require<TangemPayAddFundsComponent.Params>() private val params = paramsContainer.require<TangemPayAddFundsComponent.Params>()
@ -43,6 +45,7 @@ internal class TangemPayAddFundsModel @Inject constructor(
return TangemPayAddFundsUMConverter( return TangemPayAddFundsUMConverter(
listener = params.listener, listener = params.listener,
isRedesignEnabled = tangemPayFeatureToggles.isRedesignEnabled, isRedesignEnabled = tangemPayFeatureToggles.isRedesignEnabled,
shouldShowBankTransfer = virtualAccountToggles.isVaMvp0Enabled && params.virtualAccountOnramp != null,
).convert(data) ).convert(data)
} }

View file

@ -449,6 +449,7 @@ internal class TangemPayCardPageModel @Inject constructor(
cryptoBalance = balance.cryptoBalance.balance, cryptoBalance = balance.cryptoBalance.balance,
depositAddress = balance.cryptoBalance.depositAddress, depositAddress = balance.cryptoBalance.depositAddress,
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
virtualAccountOnramp = currentStatus.value.ifLoadedOrNull { it.virtualAccount },
), ),
) )
} }
@ -484,6 +485,12 @@ internal class TangemPayCardPageModel @Inject constructor(
) )
} }
override fun onClickBankTransfer() {
val onramp = currentStatus.value.ifLoadedOrNull { it.virtualAccount } ?: return
bottomSheetNavigation.dismiss()
bottomSheetNavigation.activate(TangemPayCardNavigation.VirtualAccountDeposit(onramp))
}
override fun onDismissAddFunds() { override fun onDismissAddFunds() {
bottomSheetNavigation.dismiss() bottomSheetNavigation.dismiss()
} }

View file

@ -210,6 +210,7 @@ internal class TangemPayDetailsModel @Inject constructor(
cryptoBalance = balance.availableForWithdrawal, cryptoBalance = balance.availableForWithdrawal,
depositAddress = balance.cryptoBalance.depositAddress, depositAddress = balance.cryptoBalance.depositAddress,
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
virtualAccountOnramp = currentStatus.value.ifLoadedOrNull { it.virtualAccount },
), ),
) )
} }
@ -344,6 +345,12 @@ internal class TangemPayDetailsModel @Inject constructor(
) )
} }
override fun onClickBankTransfer() {
val onramp = currentStatus.value.ifLoadedOrNull { it.virtualAccount } ?: return
bottomSheetNavigation.dismiss()
bottomSheetNavigation.activate(TangemPayDetailsNavigation.VirtualAccountDeposit(onramp))
}
override fun onClickReceive(data: TangemPayTopUpData) { override fun onClickReceive(data: TangemPayTopUpData) {
analytics.send(TangemPayAnalyticsEvents.ReceiveFundsClicked()) analytics.send(TangemPayAnalyticsEvents.ReceiveFundsClicked())
bottomSheetNavigation.dismiss() bottomSheetNavigation.dismiss()

View file

@ -0,0 +1,50 @@
package com.tangem.features.tangempay.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.VirtualAccountOnramp
import com.tangem.features.tangempay.components.TangemPayVirtualAccountDepositComponent
import com.tangem.features.tangempay.entity.TangemPayVirtualAccountDepositUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import javax.inject.Inject
@Stable
@ModelScoped
internal class TangemPayVirtualAccountDepositModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val urlOpener: UrlOpener,
) : Model() {
private val params = paramsContainer.require<TangemPayVirtualAccountDepositComponent.Params>()
val uiState: TangemPayVirtualAccountDepositUM = TangemPayVirtualAccountDepositUM(
fees = persistentListOf(
TangemPayVirtualAccountDepositUM.FeeRow(title = stringReference("ACH"), value = "$1"),
TangemPayVirtualAccountDepositUM.FeeRow(title = stringReference("FedWire"), value = "$11"),
),
shouldShowTermsAndConditions = params.virtualAccountOnramp is VirtualAccountOnramp.Eligible,
onShowDetailsClick = ::onShowDetailsClick,
onDismiss = ::onDismiss,
onTermsClick = { urlOpener.openUrl(TERMS_OF_USE_URL) },
onPrivacyClick = { urlOpener.openUrl(PRIVACY_POLICY_URL) },
)
fun onDismiss() {
params.onDismiss()
}
private fun onShowDetailsClick() {
// TODO([REDACTED_TASK_KEY]): VA MVP0 — open the bank-transfer requisites screen (separate PR).
}
private companion object {
const val TERMS_OF_USE_URL = "https://tangem.com/docs/en/virtual-account-terms.pdf"
const val PRIVACY_POLICY_URL = "https://tangem.com/docs/en/pay-privacy-policy.pdf"
}
}

View file

@ -1,82 +1,89 @@
package com.tangem.features.tangempay.model.transformers package com.tangem.features.tangempay.model.transformers
import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_card_20 import com.tangem.core.ui.res.generated.icons.ic_card_20
import com.tangem.core.ui.res.generated.icons.ic_logo_tangem_20 import com.tangem.core.ui.res.generated.icons.ic_logo_tangem_20
import com.tangem.core.ui.res.generated.icons.ic_sign_usd_20
import com.tangem.domain.pay.model.TangemPayTopUpData import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.features.tangempay.components.AddFundsListener import com.tangem.features.tangempay.components.AddFundsListener
import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayAddFundsItemUM import com.tangem.features.tangempay.entity.TangemPayAddFundsItemUM
import com.tangem.features.tangempay.entity.TangemPayAddFundsUM import com.tangem.features.tangempay.entity.TangemPayAddFundsUM
import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf import com.tangem.utils.extensions.addIf
import kotlinx.collections.immutable.toPersistentList
internal class TangemPayAddFundsUMConverter( internal class TangemPayAddFundsUMConverter(
val listener: AddFundsListener, val listener: AddFundsListener,
val isRedesignEnabled: Boolean, val isRedesignEnabled: Boolean,
) : Converter<TangemPayTopUpData?, TangemPayAddFundsUM> { val shouldShowBankTransfer: Boolean,
) : Converter<TangemPayTopUpData, TangemPayAddFundsUM> {
override fun convert(value: TangemPayTopUpData?): TangemPayAddFundsUM { @Suppress("UnnecessaryLet")
return if (value == null) { override fun convert(value: TangemPayTopUpData): TangemPayAddFundsUM {
TangemPayAddFundsUM( return TangemPayAddFundsUM(
items = persistentListOf(), items = buildList {
dismiss = listener::onDismissAddFunds, TangemPayAddFundsItemUM(
errorMessage = TangemPayMessagesFactory.createErrorMessage( icon = if (isRedesignEnabled) {
errorType = TangemPayDetailsErrorType.Receive, TangemIconUM.Icon(
).messageBottomSheetUM, imageVector = Icons.ic_logo_tangem_20,
) tintReference = {
} else { TangemTheme.colors3.icon.brand
TangemPayAddFundsUM( },
items = persistentListOf( )
TangemPayAddFundsItemUM( } else {
icon = if (isRedesignEnabled) { TangemIconUM.Icon(
TangemIconUM.Icon( iconRes = R.drawable.ic_exchange_vertical_24,
imageVector = Icons.ic_logo_tangem_20, tintReference = {
tintReference = { TangemTheme.colors.icon.accent
TangemTheme.colors3.icon.brand },
}, )
) },
} else { title = resourceReference(R.string.tangempay_topup_swap_title),
TangemIconUM.Icon( description = resourceReference(R.string.tangempay_topup_swap_body),
iconRes = R.drawable.ic_exchange_vertical_24, onClick = { listener.onClickSwap(value) },
tintReference = { ).let(::add)
TangemTheme.colors.icon.accent TangemPayAddFundsItemUM(
}, icon = if (isRedesignEnabled) {
) TangemIconUM.Icon(
}, imageVector = Icons.ic_card_20,
title = TextReference.Res(R.string.tangempay_topup_swap_title), tintReference = {
description = TextReference.Res(R.string.tangempay_topup_swap_body), TangemTheme.colors3.icon.brand
onClick = { listener.onClickSwap(value) }, },
), )
TangemPayAddFundsItemUM( } else {
icon = if (isRedesignEnabled) { TangemIconUM.Icon(
TangemIconUM.Icon( iconRes = R.drawable.ic_arrow_down_24,
imageVector = Icons.ic_card_20, tintReference = {
tintReference = { TangemTheme.colors.icon.accent
TangemTheme.colors3.icon.brand },
}, )
) },
} else { title = resourceReference(R.string.tangempay_topup_receive_title),
TangemIconUM.Icon( description = resourceReference(R.string.tangempay_topup_receive_body),
iconRes = R.drawable.ic_arrow_down_24, onClick = { listener.onClickReceive(value) },
tintReference = { ).let(::add)
TangemTheme.colors.icon.accent addIf(
}, condition = shouldShowBankTransfer,
) create = {
}, TangemPayAddFundsItemUM(
title = TextReference.Res(R.string.tangempay_topup_receive_title), icon = TangemIconUM.Icon(
description = TextReference.Res(R.string.tangempay_topup_receive_body), imageVector = Icons.ic_sign_usd_20,
onClick = { listener.onClickReceive(value) }, tintReference = { TangemTheme.colors3.icon.brand },
), ),
), title = stringReference("Bank transfer"),
dismiss = listener::onDismissAddFunds, description = stringReference("Receive fiat USD via ACH/FedWire"),
errorMessage = null, onClick = listener::onClickBankTransfer,
) )
} },
)
}.toPersistentList(),
dismiss = listener::onDismissAddFunds,
errorMessage = null,
)
} }
} }

View file

@ -0,0 +1,302 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.ds2.button.Close
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.row.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_info_24
import com.tangem.core.ui.res.generated.icons.ic_sign_usd_32
import com.tangem.features.tangempay.entity.TangemPayVirtualAccountDepositUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import com.tangem.core.ui.R as CoreUiR
@Composable
internal fun TangemPayVirtualAccountDepositBottomSheet(state: TangemPayVirtualAccountDepositUM) {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = state.onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
type = TangemBottomSheetType.Modal,
containerColor = TangemTheme.colors3.bg.secondary,
title = {
TangemTopBar(
type = TangemTopBarType.BottomSheet,
title = null,
endContent = { TangemButton.Close(onClick = state.onDismiss) },
)
},
content = { _ -> DepositContent(state) },
)
}
@Composable
private fun DepositContent(state: TangemPayVirtualAccountDepositUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens2.x4)
.padding(bottom = TangemTheme.dimens2.x4),
horizontalAlignment = Alignment.CenterHorizontally,
) {
IntroIcons(modifier = Modifier.padding(top = TangemTheme.dimens2.x4))
TitleText(
text = stringReference("Bank transfer might take 1-2 business days"),
modifier = Modifier.padding(top = TangemTheme.dimens2.x8),
)
SubtitleText(
text = stringReference("Received USD will be converted to USDC by 1:1 rate"),
modifier = Modifier.padding(top = TangemTheme.dimens2.x2),
)
FeesBlock(
fees = state.fees,
modifier = Modifier.padding(top = TangemTheme.dimens2.x6),
)
InfoNotification(
text = stringReference("Deposit via ACH or FedWire only. SWIFT transfers will be returned."),
modifier = Modifier.padding(top = TangemTheme.dimens2.x4),
)
TangemButton(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens2.x4),
text = stringReference("Show details"),
variant = TangemButton.Variant.Primary,
size = TangemButton.Size.X12,
onClick = state.onShowDetailsClick,
)
if (state.shouldShowTermsAndConditions) {
TermsFooter(
onTermsClick = state.onTermsClick,
onPrivacyClick = state.onPrivacyClick,
modifier = Modifier.padding(top = TangemTheme.dimens2.x3),
)
}
}
}
@Composable
private fun FeesBlock(fees: ImmutableList<TangemPayVirtualAccountDepositUM.FeeRow>, modifier: Modifier = Modifier) {
Column(modifier = modifier.fillMaxWidth()) {
Text(
modifier = Modifier.padding(
start = TangemTheme.dimens2.x4,
bottom = TangemTheme.dimens2.x2,
),
text = stringReference("Fee for onramp").resolveReference(),
style = TangemTheme.typography3.caption.medium,
color = TangemTheme.colors3.text.secondary,
)
fees.forEachIndexed { index, fee ->
TangemRow(
contentLead = TangemRowContentLead.Equal,
verticalAlignment = TangemRowVerticalAlignment.Center,
divider = index != fees.lastIndex,
titleSlot = { TangemRowText(text = fee.title, role = TangemRowTextRole.Title) },
valueSlot = { TangemRowText(text = stringReference(fee.value), role = TangemRowTextRole.Value) },
)
}
}
}
@Composable
private fun InfoNotification(text: TextReference, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens2.x4))
.background(TangemTheme.colors3.bg.status.infoSubtle)
.padding(TangemTheme.dimens2.x4),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x5),
imageVector = Icons.ic_info_24,
contentDescription = null,
tint = TangemTheme.colors3.icon.status.info,
)
Text(
text = text.resolveReference(),
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.primary,
)
}
}
@Composable
private fun TermsFooter(onTermsClick: () -> Unit, onPrivacyClick: () -> Unit, modifier: Modifier = Modifier) {
val linkStyle = SpanStyle(color = TangemTheme.colors3.text.primary)
val text = buildAnnotatedString {
append("By using service, you agree with provider ")
withLink(LinkAnnotation.Clickable(tag = "terms", linkInteractionListener = { onTermsClick() })) {
withStyle(linkStyle) { append("Terms of Use") }
}
append(" and ")
withLink(LinkAnnotation.Clickable(tag = "privacy", linkInteractionListener = { onPrivacyClick() })) {
withStyle(linkStyle) { append("Privacy Policy") }
}
}
Text(
modifier = modifier.fillMaxWidth(),
text = text,
style = TangemTheme.typography3.caption.medium,
color = TangemTheme.colors3.text.secondary,
textAlign = TextAlign.Center,
)
}
@Composable
private fun IntroIcons(modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(-TangemTheme.dimens2.x4),
) {
UsdIcon()
UsdcIcon()
}
}
@Composable
private fun UsdIcon(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens2.x20)
.clip(CircleShape)
.background(TangemTheme.colors3.bg.opaque.primary)
.border(width = 1.dp, color = TangemTheme.colors3.border.secondary, shape = CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x8),
imageVector = Icons.ic_sign_usd_32,
contentDescription = null,
tint = TangemTheme.colors3.icon.primary,
)
}
}
@Composable
private fun UsdcIcon(modifier: Modifier = Modifier) {
Box(modifier = modifier.size(TangemTheme.dimens2.x20)) {
Image(
modifier = Modifier
.fillMaxSize()
.clip(CircleShape)
.border(width = 1.dp, color = TangemTheme.colors3.border.secondary, shape = CircleShape),
painter = painterResource(CoreUiR.drawable.img_usdc_16),
contentDescription = null,
)
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(TangemTheme.dimens2.x6)
.background(color = TangemTheme.colors3.bg.accent.violet, shape = CircleShape)
.border(
width = TangemTheme.dimens2.x0_5,
color = TangemTheme.colors3.bg.secondary,
shape = CircleShape,
),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x4),
painter = painterResource(CoreUiR.drawable.ic_polygon_22),
contentDescription = null,
tint = TangemTheme.colors3.icon.inverse,
)
}
}
}
@Composable
private fun TitleText(text: TextReference, modifier: Modifier = Modifier) {
Text(
modifier = modifier.fillMaxWidth(),
text = text.resolveReference(),
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary,
textAlign = TextAlign.Center,
)
}
@Composable
private fun SubtitleText(text: TextReference, modifier: Modifier = Modifier) {
Text(
modifier = modifier.fillMaxWidth(),
text = text.resolveReference(),
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.secondary,
textAlign = TextAlign.Center,
)
}
private fun previewState(shouldShowTermsAndConditions: Boolean) = TangemPayVirtualAccountDepositUM(
fees = persistentListOf(
TangemPayVirtualAccountDepositUM.FeeRow(title = stringReference("ACH"), value = "$1"),
TangemPayVirtualAccountDepositUM.FeeRow(title = stringReference("FedWire"), value = "$11"),
),
shouldShowTermsAndConditions = shouldShowTermsAndConditions,
onShowDetailsClick = {},
onDismiss = {},
onTermsClick = {},
onPrivacyClick = {},
)
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun DepositEligiblePreview() {
TangemThemePreviewRedesign {
DepositContent(
state = previewState(shouldShowTermsAndConditions = true),
modifier = Modifier.background(TangemTheme.colors3.bg.secondary),
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun DepositAvailablePreview() {
TangemThemePreviewRedesign {
DepositContent(
state = previewState(shouldShowTermsAndConditions = false),
modifier = Modifier.background(TangemTheme.colors3.bg.secondary),
)
}
}