Updated on 2026-08-14
This commit is contained in:
parent
53bde31f89
commit
7443421bb1
20 changed files with 852 additions and 77 deletions
|
|
@ -5,7 +5,6 @@ import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
|||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
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.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
|
|
@ -119,7 +118,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
)
|
||||
},
|
||||
error = null,
|
||||
virtualAccount = VirtualAccountOnramp.None,
|
||||
virtualAccount = null,
|
||||
)
|
||||
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
|
||||
source = StatusSource.CACHE,
|
||||
|
|
|
|||
|
|
@ -396,11 +396,10 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
* 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
|
||||
* the `VISA_VIRTUAL_ACCOUNT` eligibility channel (fetched fresh via the user token), else
|
||||
* [VirtualAccountOnramp.None].
|
||||
* the `VISA_VIRTUAL_ACCOUNT` eligibility channel (fetched fresh via the user token), else `null`.
|
||||
*/
|
||||
private suspend fun CustomerInfo.resolveVirtualAccountOnramp(userWalletId: UserWalletId): VirtualAccountOnramp {
|
||||
if (!virtualAccountFeatureToggles.isVaMvp0Enabled) return VirtualAccountOnramp.None
|
||||
private suspend fun CustomerInfo.resolveVirtualAccountOnramp(userWalletId: UserWalletId): VirtualAccountOnramp? {
|
||||
if (!virtualAccountFeatureToggles.isVaMvp0Enabled) return null
|
||||
|
||||
val accountInstance = productInstances.firstOrNull {
|
||||
it.specificationDataType == SpecificationDataType.ACCOUNT
|
||||
|
|
@ -409,7 +408,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
return onboardingRepository.getBankCredentials(userWalletId, accountInstance.id).fold(
|
||||
ifLeft = { error ->
|
||||
logger.e("getBankCredentials failed for ${accountInstance.id}: $error")
|
||||
VirtualAccountOnramp.None
|
||||
null
|
||||
},
|
||||
ifRight = { credentials ->
|
||||
VirtualAccountOnramp.Available(
|
||||
|
|
@ -423,13 +422,13 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
return onboardingRepository.fetchCustomerEligibility(userWalletId).fold(
|
||||
ifLeft = { error ->
|
||||
logger.e("fetchCustomerEligibility failed for $userWalletId: $error")
|
||||
VirtualAccountOnramp.None
|
||||
null
|
||||
},
|
||||
ifRight = { channels ->
|
||||
if (channels.contains(TangemPayEligibilityType.VISA_VIRTUAL_ACCOUNT)) {
|
||||
VirtualAccountOnramp.Eligible
|
||||
} else {
|
||||
VirtualAccountOnramp.None
|
||||
null
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -149,7 +149,8 @@ sealed class PaymentAccountStatusValue {
|
|||
* [totalFiatBalance] resolves to [TotalFiatBalance.Failed].
|
||||
* @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.
|
||||
* @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.
|
||||
*/
|
||||
@Serializable
|
||||
|
|
@ -162,7 +163,7 @@ sealed class PaymentAccountStatusValue {
|
|||
val cards: List<TangemPayCard>,
|
||||
val fiatRate: SerializedBigDecimal?,
|
||||
val error: Error?,
|
||||
val virtualAccount: VirtualAccountOnramp,
|
||||
val virtualAccount: VirtualAccountOnramp?,
|
||||
) : PaymentAccountStatusValue() {
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||
currency = cryptoCurrency,
|
||||
|
|
|
|||
|
|
@ -11,10 +11,6 @@ import kotlinx.serialization.Serializable
|
|||
@Serializable
|
||||
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`). */
|
||||
@Serializable
|
||||
data object Eligible : VirtualAccountOnramp
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ dependencies {
|
|||
implementation(projects.features.tokenRecieve.api)
|
||||
implementation(projects.features.txhistory.api)
|
||||
implementation(projects.features.tokendetails.api)
|
||||
implementation(projects.features.virtualAccounts.details.api) // TWI_1638_VA_MVP0_ENABLED
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.balanceHiding)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.TangemPayTopUpData
|
||||
|
|
@ -39,11 +40,13 @@ internal class TangemPayAddFundsComponent(
|
|||
val fiatBalance: BigDecimal,
|
||||
val depositAddress: String,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val virtualAccountOnramp: VirtualAccountOnramp?,
|
||||
)
|
||||
}
|
||||
|
||||
internal interface AddFundsListener {
|
||||
fun onClickReceive(data: TangemPayTopUpData)
|
||||
fun onClickSwap(data: TangemPayTopUpData)
|
||||
fun onClickBankTransfer()
|
||||
fun onDismissAddFunds()
|
||||
}
|
||||
|
|
@ -97,6 +97,14 @@ internal class TangemPayCardPageScreenComponent(
|
|||
fiatBalance = navigation.fiatBalance,
|
||||
depositAddress = navigation.depositAddress,
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -126,6 +126,14 @@ internal class TangemPayDetailsComponent(
|
|||
depositAddress = navigation.depositAddress,
|
||||
cryptoCurrency = navigation.cryptoCurrency,
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -45,6 +45,11 @@ internal interface TangemPayModelModule {
|
|||
@ClassKey(TangemPayAddFundsModel::class)
|
||||
fun bindTangemPayAddFundsModel(model: TangemPayAddFundsModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TangemPayVirtualAccountDepositModel::class)
|
||||
fun bindTangemPayVirtualAccountDepositModel(model: TangemPayVirtualAccountDepositModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TangemPayViewPinModel::class)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.tangempay.entity
|
||||
|
||||
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.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -30,6 +31,12 @@ internal sealed class TangemPayCardNavigation {
|
|||
val fiatBalance: SerializedBigDecimal,
|
||||
val depositAddress: String,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val virtualAccountOnramp: VirtualAccountOnramp?,
|
||||
) : TangemPayCardNavigation()
|
||||
|
||||
@Serializable
|
||||
data class VirtualAccountDeposit(
|
||||
val virtualAccountOnramp: VirtualAccountOnramp,
|
||||
) : TangemPayCardNavigation()
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.tangempay.entity
|
||||
|
||||
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.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.serialization.SerializedCurrency
|
||||
|
|
@ -21,6 +22,12 @@ internal sealed class TangemPayDetailsNavigation {
|
|||
val fiatBalance: SerializedBigDecimal,
|
||||
val depositAddress: String,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val virtualAccountOnramp: VirtualAccountOnramp?,
|
||||
) : TangemPayDetailsNavigation()
|
||||
|
||||
@Serializable
|
||||
data class VirtualAccountDeposit(
|
||||
val virtualAccountOnramp: VirtualAccountOnramp,
|
||||
) : TangemPayDetailsNavigation()
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import com.tangem.features.tangempay.TangemPayFeatureToggles
|
|||
import com.tangem.features.tangempay.components.TangemPayAddFundsComponent
|
||||
import com.tangem.features.tangempay.entity.TangemPayAddFundsUM
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayAddFundsUMConverter
|
||||
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ internal class TangemPayAddFundsModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
private val virtualAccountToggles: VirtualAccountFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TangemPayAddFundsComponent.Params>()
|
||||
|
|
@ -43,6 +45,7 @@ internal class TangemPayAddFundsModel @Inject constructor(
|
|||
return TangemPayAddFundsUMConverter(
|
||||
listener = params.listener,
|
||||
isRedesignEnabled = tangemPayFeatureToggles.isRedesignEnabled,
|
||||
shouldShowBankTransfer = virtualAccountToggles.isVaMvp0Enabled && params.virtualAccountOnramp != null,
|
||||
).convert(data)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -449,6 +449,7 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
cryptoBalance = balance.cryptoBalance.balance,
|
||||
depositAddress = balance.cryptoBalance.depositAddress,
|
||||
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() {
|
||||
bottomSheetNavigation.dismiss()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
cryptoBalance = balance.availableForWithdrawal,
|
||||
depositAddress = balance.cryptoBalance.depositAddress,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
virtualAccountOnramp = currentStatus.value.ifLoadedOrNull { it.virtualAccount },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -309,6 +310,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) {
|
||||
analytics.send(TangemPayAnalyticsEvents.ReceiveFundsClicked())
|
||||
bottomSheetNavigation.dismiss()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +1,32 @@
|
|||
package com.tangem.features.tangempay.model.transformers
|
||||
|
||||
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.generated.icons.Icons
|
||||
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_sign_usd_20
|
||||
import com.tangem.domain.pay.model.TangemPayTopUpData
|
||||
import com.tangem.features.tangempay.components.AddFundsListener
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayAddFundsItemUM
|
||||
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 kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.utils.extensions.addIf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class TangemPayAddFundsUMConverter(
|
||||
val listener: AddFundsListener,
|
||||
val isRedesignEnabled: Boolean,
|
||||
) : Converter<TangemPayTopUpData?, TangemPayAddFundsUM> {
|
||||
val shouldShowBankTransfer: Boolean,
|
||||
) : Converter<TangemPayTopUpData, TangemPayAddFundsUM> {
|
||||
|
||||
override fun convert(value: TangemPayTopUpData?): TangemPayAddFundsUM {
|
||||
return if (value == null) {
|
||||
TangemPayAddFundsUM(
|
||||
items = persistentListOf(),
|
||||
dismiss = listener::onDismissAddFunds,
|
||||
errorMessage = TangemPayMessagesFactory.createErrorMessage(
|
||||
errorType = TangemPayDetailsErrorType.Receive,
|
||||
).messageBottomSheetUM,
|
||||
)
|
||||
} else {
|
||||
TangemPayAddFundsUM(
|
||||
items = persistentListOf(
|
||||
@Suppress("UnnecessaryLet")
|
||||
override fun convert(value: TangemPayTopUpData): TangemPayAddFundsUM {
|
||||
return TangemPayAddFundsUM(
|
||||
items = buildList {
|
||||
TangemPayAddFundsItemUM(
|
||||
icon = if (isRedesignEnabled) {
|
||||
TangemIconUM.Icon(
|
||||
|
|
@ -49,10 +43,10 @@ internal class TangemPayAddFundsUMConverter(
|
|||
},
|
||||
)
|
||||
},
|
||||
title = TextReference.Res(R.string.tangempay_topup_swap_title),
|
||||
description = TextReference.Res(R.string.tangempay_topup_swap_body),
|
||||
title = resourceReference(R.string.tangempay_topup_swap_title),
|
||||
description = resourceReference(R.string.tangempay_topup_swap_body),
|
||||
onClick = { listener.onClickSwap(value) },
|
||||
),
|
||||
).let(::add)
|
||||
TangemPayAddFundsItemUM(
|
||||
icon = if (isRedesignEnabled) {
|
||||
TangemIconUM.Icon(
|
||||
|
|
@ -69,14 +63,27 @@ internal class TangemPayAddFundsUMConverter(
|
|||
},
|
||||
)
|
||||
},
|
||||
title = TextReference.Res(R.string.tangempay_topup_receive_title),
|
||||
description = TextReference.Res(R.string.tangempay_topup_receive_body),
|
||||
title = resourceReference(R.string.tangempay_topup_receive_title),
|
||||
description = resourceReference(R.string.tangempay_topup_receive_body),
|
||||
onClick = { listener.onClickReceive(value) },
|
||||
).let(::add)
|
||||
addIf(
|
||||
condition = shouldShowBankTransfer,
|
||||
create = {
|
||||
TangemPayAddFundsItemUM(
|
||||
icon = TangemIconUM.Icon(
|
||||
imageVector = Icons.ic_sign_usd_20,
|
||||
tintReference = { TangemTheme.colors3.icon.brand },
|
||||
),
|
||||
),
|
||||
title = stringReference("Bank transfer"),
|
||||
description = stringReference("Receive fiat USD via ACH/FedWire"),
|
||||
onClick = listener::onClickBankTransfer,
|
||||
)
|
||||
},
|
||||
)
|
||||
}.toPersistentList(),
|
||||
dismiss = listener::onDismissAddFunds,
|
||||
errorMessage = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue