Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-29 18:24:12 +05:00
parent 4faf6e9cb0
commit e06dc1297d
16 changed files with 287 additions and 8 deletions

View file

@ -23,6 +23,13 @@ interface TangemPayApi {
@GET("v1/customer/me") @GET("v1/customer/me")
suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse<CustomerMeResponse> suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse<CustomerMeResponse>
/** Fiat bank requisites for the Virtual Account on-ramp (VA MVP0, TWI-1638). */
@GET("v1/account/bank-credentials/{product_instance_id}")
suspend fun getBankCredentials(
@Header("Authorization") authHeader: String,
@Path("product_instance_id") productInstanceId: String,
): ApiResponse<BankCredentialsResponse>
@GET("v1/customer/wallets/{customer_wallet_id}") @GET("v1/customer/wallets/{customer_wallet_id}")
suspend fun checkCustomerWalletId( suspend fun checkCustomerWalletId(
@Path("customer_wallet_id") customerWalletId: String, @Path("customer_wallet_id") customerWalletId: String,
@ -40,6 +47,12 @@ interface TangemPayApi {
@GET("v1/eligibility/channels") @GET("v1/eligibility/channels")
suspend fun getEligibilityChannels(): ApiResponse<TangemPayEligibilityChannels> suspend fun getEligibilityChannels(): ApiResponse<TangemPayEligibilityChannels>
/** Eligibility channels fetched with the user (customer-wallet) token (VA MVP0, TWI-1638). */
@GET("v1/eligibility/channels")
suspend fun getUserEligibilityChannels(
@Header("Authorization") authHeader: String,
): ApiResponse<TangemPayEligibilityChannels>
@GET("v1/order/{order_id}") @GET("v1/order/{order_id}")
suspend fun getOrder( suspend fun getOrder(
@Header("Authorization") authHeader: String, @Header("Authorization") authHeader: String,

View file

@ -0,0 +1,19 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Response of `bff-v2/v1/account/bank-credentials/{product_instance_id}` fiat bank requisites for the
* Virtual Account on-ramp (VA MVP0, TWI-1638).
*/
@JsonClass(generateAdapter = true)
data class BankCredentialsResponse(
@Json(name = "type") val type: String?,
@Json(name = "beneficiary_name") val beneficiaryName: String?,
@Json(name = "beneficiary_address") val beneficiaryAddress: String?,
@Json(name = "beneficiary_bank_name") val beneficiaryBankName: String?,
@Json(name = "beneficiary_bank_address") val beneficiaryBankAddress: String?,
@Json(name = "account_number") val accountNumber: String?,
@Json(name = "routing_number") val routingNumber: String?,
)

View file

@ -48,6 +48,7 @@ dependencies {
implementation(projects.domain.quotes) implementation(projects.domain.quotes)
implementation(projects.domain.common) implementation(projects.domain.common)
implementation(projects.features.swap.domain) implementation(projects.features.swap.domain)
implementation(projects.features.virtualAccounts.details.api)
/** Project - Utils */ /** Project - Utils */

View file

@ -5,6 +5,7 @@ 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
@ -118,6 +119,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
) )
}, },
error = null, error = null,
virtualAccount = VirtualAccountOnramp.None,
) )
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview( is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
source = StatusSource.CACHE, source = StatusSource.CACHE,

View file

@ -4,21 +4,16 @@ import arrow.core.Either
import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.data.pay.store.PaymentAccountStatusesStore
import com.tangem.domain.core.utils.catchOn import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.*
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.account.hasAccountData
import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.pay.TangemPayCard import com.tangem.domain.models.pay.*
import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardLimitData
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.quote.QuoteStatus
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
import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.SpecificationDataType
import com.tangem.domain.pay.model.OrderData import com.tangem.domain.pay.model.OrderData
import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayEntryPoint import com.tangem.domain.pay.model.TangemPayEntryPoint
@ -26,6 +21,7 @@ import com.tangem.domain.pay.repository.*
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaApiError
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.security.isSecurityExposed import com.tangem.security.isSecurityExposed
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -66,6 +62,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private val closeCardRepository: TangemPayCloseCardRepository, private val closeCardRepository: TangemPayCloseCardRepository,
private val cardDetailsRepository: TangemPayCardDetailsRepository, private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val issueCardRepository: TangemPayIssueCardRepository, private val issueCardRepository: TangemPayIssueCardRepository,
private val virtualAccountFeatureToggles: VirtualAccountFeatureToggles,
) : PaymentAccountStatusFetcher { ) : PaymentAccountStatusFetcher {
private val logger = TangemLogger.withTag(TAG) private val logger = TangemLogger.withTag(TAG)
@ -382,6 +379,8 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
// the previously shown order and append newly seen cards at the end. // the previously shown order and append newly seen cards at the end.
val orderedCards = tangemPayCards.stableOrder(previousRealCardOrder(userWalletId)) val orderedCards = tangemPayCards.stableOrder(previousRealCardOrder(userWalletId))
val virtualAccount = resolveVirtualAccountOnramp(userWalletId)
return PaymentAccountStatusValue.Loaded( return PaymentAccountStatusValue.Loaded(
source = StatusSource.ACTUAL, source = StatusSource.ACTUAL,
customerId = customerId, customerId = customerId,
@ -395,6 +394,50 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
availableForWithdrawal = availableForWithdrawal.orZero(), availableForWithdrawal = availableForWithdrawal.orZero(),
), ),
error = null, error = null,
virtualAccount = virtualAccount,
)
}
/**
* 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].
*/
private suspend fun CustomerInfo.resolveVirtualAccountOnramp(userWalletId: UserWalletId): VirtualAccountOnramp {
if (!virtualAccountFeatureToggles.isVaMvp0Enabled) return VirtualAccountOnramp.None
val accountInstance = productInstances.firstOrNull {
it.specificationDataType == SpecificationDataType.ACCOUNT
}
if (accountInstance != null) {
return onboardingRepository.getBankCredentials(userWalletId, accountInstance.id).fold(
ifLeft = { error ->
logger.e("getBankCredentials failed for ${accountInstance.id}: $error")
VirtualAccountOnramp.None
},
ifRight = { credentials ->
VirtualAccountOnramp.Available(
productInstanceId = accountInstance.id,
bankCredentials = credentials,
)
},
)
}
return onboardingRepository.fetchCustomerEligibility(userWalletId).fold(
ifLeft = { error ->
logger.e("fetchCustomerEligibility failed for $userWalletId: $error")
VirtualAccountOnramp.None
},
ifRight = { channels ->
if (channels.contains(TangemPayEligibilityType.VISA_VIRTUAL_ACCOUNT)) {
VirtualAccountOnramp.Eligible
} else {
VirtualAccountOnramp.None
}
},
) )
} }

View file

@ -6,6 +6,7 @@ import arrow.core.left
import arrow.core.right import arrow.core.right
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.data.pay.store.PaymentAccountStatusesStore
import com.tangem.data.pay.util.BankCredentialsConverter
import com.tangem.data.pay.util.CustomerInfoConverter import com.tangem.data.pay.util.CustomerInfoConverter
import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
@ -18,6 +19,7 @@ import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.BankCredentials
import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.pay.TangemPayEligibilityType import com.tangem.domain.models.pay.TangemPayEligibilityType
@ -105,6 +107,15 @@ internal class DefaultOnboardingRepository @Inject constructor(
} }
} }
override suspend fun getBankCredentials(
userWalletId: UserWalletId,
productInstanceId: String,
): Either<VisaApiError, BankCredentials> {
return requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getBankCredentials(authHeader = authHeader, productInstanceId = productInstanceId)
}.map { response -> BankCredentialsConverter.convert(response) }
}
override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean { override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean {
return tangemPayStorage.isTangemPayDeactivated(userWalletId) return tangemPayStorage.isTangemPayDeactivated(userWalletId)
} }
@ -226,6 +237,16 @@ internal class DefaultOnboardingRepository @Inject constructor(
return tangemPayStorage.getTangemPayEligibility().map(TangemPayEligibilityType::fromString) return tangemPayStorage.getTangemPayEligibility().map(TangemPayEligibilityType::fromString)
} }
override suspend fun fetchCustomerEligibility(
userWalletId: UserWalletId,
): Either<VisaApiError, List<TangemPayEligibilityType>> {
return requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getUserEligibilityChannels(authHeader)
}.map { response ->
response.result.channels.map(TangemPayEligibilityType::fromString)
}
}
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean { override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
return tangemPayStorage.getHideMainOnboardingBanner(userWalletId) return tangemPayStorage.getHideMainOnboardingBanner(userWalletId)
} }

View file

@ -0,0 +1,19 @@
package com.tangem.data.pay.util
import com.tangem.datasource.api.pay.models.response.BankCredentialsResponse
import com.tangem.domain.models.account.BankCredentials
import com.tangem.utils.converter.Converter
internal object BankCredentialsConverter : Converter<BankCredentialsResponse, BankCredentials> {
override fun convert(value: BankCredentialsResponse): BankCredentials {
return BankCredentials(
type = value.type.orEmpty(),
beneficiaryName = value.beneficiaryName.orEmpty(),
beneficiaryAddress = value.beneficiaryAddress.orEmpty(),
beneficiaryBankName = value.beneficiaryBankName.orEmpty(),
beneficiaryBankAddress = value.beneficiaryBankAddress.orEmpty(),
accountNumber = value.accountNumber.orEmpty(),
routingNumber = value.routingNumber.orEmpty(),
)
}
}

View file

@ -14,6 +14,7 @@ import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.CustomerInfo.CardInfo import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.SpecificationDataType
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.Status import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.Status
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero import com.tangem.utils.extensions.orZero
@ -62,6 +63,7 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
displayName = if (name != null) CardDisplayName(name).getOrElse { null } else null, displayName = if (name != null) CardDisplayName(name).getOrElse { null } else null,
actualCardLimit = actualCardLimit?.parseCardLimit(), actualCardLimit = actualCardLimit?.parseCardLimit(),
adminCardLimit = adminCardLimit?.parseCardLimit(), adminCardLimit = adminCardLimit?.parseCardLimit(),
specificationDataType = specificationDataType.toDomain(),
) )
} }
@ -108,4 +110,10 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
CustomerMeResponse.ProductInstance.Status.CANCELED -> Status.CANCELED CustomerMeResponse.ProductInstance.Status.CANCELED -> Status.CANCELED
CustomerMeResponse.ProductInstance.Status.UNKNOWN -> Status.UNKNOWN CustomerMeResponse.ProductInstance.Status.UNKNOWN -> Status.UNKNOWN
} }
private fun CustomerMeResponse.ProductInstance.SpecificationDataType.toDomain(): SpecificationDataType =
when (this) {
CustomerMeResponse.ProductInstance.SpecificationDataType.ACCOUNT -> SpecificationDataType.ACCOUNT
CustomerMeResponse.ProductInstance.SpecificationDataType.CARD -> SpecificationDataType.CARD
}
} }

View file

@ -6,6 +6,7 @@ import com.tangem.core.error.UniversalError
import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.domain.models.account.BankCredentials
import com.tangem.domain.models.pay.TangemPayEligibilityType import com.tangem.domain.models.pay.TangemPayEligibilityType
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.CustomerInfo
@ -47,6 +48,11 @@ internal class MockAwareOnboardingRepository @Inject constructor(
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> = override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> =
real.getCustomerInfo(userWalletId) real.getCustomerInfo(userWalletId)
override suspend fun getBankCredentials(
userWalletId: UserWalletId,
productInstanceId: String,
): Either<VisaApiError, BankCredentials> = real.getBankCredentials(userWalletId, productInstanceId)
override suspend fun createOrder(userWalletId: UserWalletId): Either<VisaApiError, String> { override suspend fun createOrder(userWalletId: UserWalletId): Either<VisaApiError, String> {
if (isMockMode) { if (isMockMode) {
mockOrderIds.add(userWalletId) mockOrderIds.add(userWalletId)
@ -77,6 +83,10 @@ internal class MockAwareOnboardingRepository @Inject constructor(
override suspend fun getCustomerEligibility(): List<TangemPayEligibilityType> = override suspend fun getCustomerEligibility(): List<TangemPayEligibilityType> =
real.getCustomerEligibility() real.getCustomerEligibility()
override suspend fun fetchCustomerEligibility(
userWalletId: UserWalletId,
): Either<VisaApiError, List<TangemPayEligibilityType>> = real.fetchCustomerEligibility(userWalletId)
override fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? = override fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? =
real.getSavedCustomerInfo(userWalletId) real.getSavedCustomerInfo(userWalletId)

View file

@ -0,0 +1,67 @@
package com.tangem.data.pay.util
import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.api.pay.models.response.BankCredentialsResponse
import com.tangem.domain.models.account.BankCredentials
import org.junit.jupiter.api.Test
internal class BankCredentialsConverterTest {
@Test
fun `GIVEN full response WHEN convert THEN all fields mapped`() {
// Arrange
val response = BankCredentialsResponse(
type = "fiat",
beneficiaryName = "Ivan Ivanov",
beneficiaryAddress = "18, Rue Rubens 20, Paris, Ile-de-France 75013, US",
beneficiaryBankName = "SSB BANK",
beneficiaryBankAddress = "8700 Perry Highway, Pittsburgh, PA 15237, US",
accountNumber = "707613210122",
routingNumber = "043087080",
)
// Act
val actual = BankCredentialsConverter.convert(response)
// Assert
val expected = BankCredentials(
type = "fiat",
beneficiaryName = "Ivan Ivanov",
beneficiaryAddress = "18, Rue Rubens 20, Paris, Ile-de-France 75013, US",
beneficiaryBankName = "SSB BANK",
beneficiaryBankAddress = "8700 Perry Highway, Pittsburgh, PA 15237, US",
accountNumber = "707613210122",
routingNumber = "043087080",
)
assertThat(actual).isEqualTo(expected)
}
@Test
fun `GIVEN null fields WHEN convert THEN mapped to empty strings`() {
// Arrange
val response = BankCredentialsResponse(
type = null,
beneficiaryName = null,
beneficiaryAddress = null,
beneficiaryBankName = null,
beneficiaryBankAddress = null,
accountNumber = null,
routingNumber = null,
)
// Act
val actual = BankCredentialsConverter.convert(response)
// Assert
val expected = BankCredentials(
type = "",
beneficiaryName = "",
beneficiaryAddress = "",
beneficiaryBankName = "",
beneficiaryBankAddress = "",
accountNumber = "",
routingNumber = "",
)
assertThat(actual).isEqualTo(expected)
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.domain.models.account
import kotlinx.serialization.Serializable
/**
* Bank (fiat) credentials for a Virtual Account on-ramp the wire/ACH requisites a user transfers funds to.
*
* Returned by `bff-v2/v1/account/bank-credentials/{product_instance_id}`. Sensitive data kept transient
* (never persisted in the local payment-account cache).
*/
@Serializable
data class BankCredentials(
val type: String,
val beneficiaryName: String,
val beneficiaryAddress: String,
val beneficiaryBankName: String,
val beneficiaryBankAddress: String,
val accountNumber: String,
val routingNumber: String,
)

View file

@ -149,6 +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).
* Transient: not persisted in the local cache.
*/ */
@Serializable @Serializable
data class Loaded( data class Loaded(
@ -160,6 +162,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,
) : PaymentAccountStatusValue() { ) : PaymentAccountStatusValue() {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency, currency = cryptoCurrency,

View file

@ -0,0 +1,28 @@
package com.tangem.domain.models.account
import kotlinx.serialization.Serializable
/**
* Virtual Account (Visa on-ramp) availability for a payment account VA MVP0 (TWI-1638).
*
* Computed in the payment-account fetcher and surfaced on [PaymentAccountStatusValue.Loaded].
* Transient: [Available.bankCredentials] is never persisted in the local cache.
*/
@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
/** VA product instance exists; [bankCredentials] are the fiat requisites for the bank-transfer top-up. */
@Serializable
data class Available(
val productInstanceId: String,
val bankCredentials: BankCredentials,
) : VirtualAccountOnramp
}

View file

@ -10,6 +10,8 @@ enum class TangemPayEligibilityType {
DETAILS_VIRTUAL_ACCOUNT, DETAILS_VIRTUAL_ACCOUNT,
DEEPLINK_VIRTUAL_ACCOUNT, DEEPLINK_VIRTUAL_ACCOUNT,
VISA_VIRTUAL_ACCOUNT,
UNKNOWN, UNKNOWN,
; ;
@ -21,6 +23,7 @@ enum class TangemPayEligibilityType {
"BANNER_VIRTUAL_ACCOUNT" -> BANNER_VIRTUAL_ACCOUNT "BANNER_VIRTUAL_ACCOUNT" -> BANNER_VIRTUAL_ACCOUNT
"DETAILS_VIRTUAL_ACCOUNT" -> DETAILS_VIRTUAL_ACCOUNT "DETAILS_VIRTUAL_ACCOUNT" -> DETAILS_VIRTUAL_ACCOUNT
"DEEPLINK_VIRTUAL_ACCOUNT" -> DEEPLINK_VIRTUAL_ACCOUNT "DEEPLINK_VIRTUAL_ACCOUNT" -> DEEPLINK_VIRTUAL_ACCOUNT
"VISA_VIRTUAL_ACCOUNT" -> VISA_VIRTUAL_ACCOUNT
else -> UNKNOWN else -> UNKNOWN
} }
} }

View file

@ -67,6 +67,7 @@ data class CustomerInfo(
val actualCardLimit: TangemPayCardLimit?, val actualCardLimit: TangemPayCardLimit?,
val adminCardLimit: TangemPayCardLimit?, val adminCardLimit: TangemPayCardLimit?,
val status: Status, val status: Status,
val specificationDataType: SpecificationDataType,
) { ) {
enum class Status { enum class Status {
NEW, NEW,
@ -82,6 +83,12 @@ data class CustomerInfo(
CANCELED, CANCELED,
UNKNOWN, UNKNOWN,
} }
/** `ACCOUNT` marks a Virtual Account instance (vs. a `CARD`); used by VA MVP0 (TWI-1638). */
enum class SpecificationDataType {
ACCOUNT,
CARD,
}
} }
data class CardInfo( data class CardInfo(

View file

@ -2,6 +2,7 @@ package com.tangem.domain.pay.repository
import arrow.core.Either import arrow.core.Either
import com.tangem.core.error.UniversalError import com.tangem.core.error.UniversalError
import com.tangem.domain.models.account.BankCredentials
import com.tangem.domain.models.pay.TangemPayEligibilityType import com.tangem.domain.models.pay.TangemPayEligibilityType
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.CustomerInfo
@ -17,6 +18,12 @@ interface OnboardingRepository {
suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo>
/** Fiat bank requisites for the wallet's Virtual Account on-ramp instance (VA MVP0, TWI-1638). */
suspend fun getBankCredentials(
userWalletId: UserWalletId,
productInstanceId: String,
): Either<VisaApiError, BankCredentials>
suspend fun createOrder(userWalletId: UserWalletId): Either<VisaApiError, String> suspend fun createOrder(userWalletId: UserWalletId): Either<VisaApiError, String>
suspend fun clearOrderId(userWalletId: UserWalletId) suspend fun clearOrderId(userWalletId: UserWalletId)
@ -28,6 +35,14 @@ interface OnboardingRepository {
suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType> suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType>
suspend fun getCustomerEligibility(): List<TangemPayEligibilityType> suspend fun getCustomerEligibility(): List<TangemPayEligibilityType>
/**
* Fetches eligibility channels fresh via the user token (always hits the network, no cache read/write).
* Differs from [checkCustomerEligibility] (static token, caches) and [getCustomerEligibility] (cache only).
*/
suspend fun fetchCustomerEligibility(
userWalletId: UserWalletId,
): Either<VisaApiError, List<TangemPayEligibilityType>>
fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo?
suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean