Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-23 03:02:00 -07:00
parent e1c819d421
commit 53b02cfb10
31 changed files with 286 additions and 346 deletions

View file

@ -51,7 +51,7 @@ class MoshiModule {
.withSubtype(PaymentAccountStatusValueDM.NotCreated::class.java, "not_created")
.withSubtype(PaymentAccountStatusValueDM.UnderReview::class.java, "kyc_status")
.withSubtype(PaymentAccountStatusValueDM.IssuingCard::class.java, "issuing_card")
.withSubtype(PaymentAccountStatusValueDM.ActiveCard::class.java, "active_card")
.withSubtype(PaymentAccountStatusValueDM.ActiveAccount::class.java, "active_account")
.withSubtype(PaymentAccountStatusValueDM.CardIssueFailed::class.java, "card_issue_failed"),
)
.add(

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.local.visa
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.domain.models.TangemPayReissueCardFee
import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.models.wallet.UserWalletId
internal class DefaultTangemPayReissueCardStore(

View file

@ -1,6 +1,6 @@
package com.tangem.datasource.local.visa
import com.tangem.domain.models.TangemPayReissueCardFee
import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.models.wallet.UserWalletId
interface TangemPayReissueCardStore {

View file

@ -4,6 +4,7 @@ package com.tangem.datasource.local.visa.entity
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.serialization.SerializedBigDecimal
import dev.onenowy.moshipolymorphicadapter.PolymorphicAdapterType
import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
import java.math.BigDecimal
@ -32,18 +33,14 @@ sealed interface PaymentAccountStatusValueDM {
@Json(name = "issuing_card") val marker: Boolean = true,
) : PaymentAccountStatusValueDM
@NameLabel("active_card")
data class ActiveCard(
@Json(name = "active_card") val isLocked: Boolean,
@NameLabel("active_account")
data class ActiveAccount(
@Json(name = "customer_id") val customerId: String,
@Json(name = "card_id") val cardId: String,
@Json(name = "last_four_digits") val lastFourDigits: String,
@Json(name = "currency_code") val currencyCode: String,
@Json(name = "deposit_address") val depositAddress: String?,
@Json(name = "is_pin_set") val isPinSet: Boolean,
@Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
@Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM,
@Json(name = "display_name") val displayName: String?,
@Json(name = "cards") val cards: List<TangemPayCard>,
) : PaymentAccountStatusValueDM
@NameLabel("card_issue_failed")
@ -66,4 +63,15 @@ sealed interface PaymentAccountStatusValueDM {
@Json(name = "token_contract_address") val tokenContractAddress: String,
@Json(name = "balance") val balance: BigDecimal,
)
@JsonClass(generateAdapter = true)
data class TangemPayCard(
@Json(name = "id") val id: String,
@Json(name = "has_pin_code") val hasPinCode: Boolean,
@Json(name = "display_name") val displayName: String?,
@Json(name = "actual_daily_limit") val actualDailyLimit: SerializedBigDecimal?,
@Json(name = "admin_daily_limit") val adminDailyLimit: SerializedBigDecimal?,
@Json(name = "is_frozen") val isFrozen: Boolean,
@Json(name = "last_digits") val lastDigits: String,
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.utils
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
@ -34,7 +35,11 @@ class MoshiDataStoreSerializer<T>(
override suspend fun readFrom(input: InputStream): T {
return input.bufferedReader().use { reader ->
adapter.fromJson(reader.readText()) ?: defaultValue
try {
adapter.fromJson(reader.readText()) ?: defaultValue
} catch (e: Exception) {
throw CorruptionException("Failed to deserialize data", e)
}
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.data.pay
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.TangemPayEligibilityType
import com.tangem.domain.models.pay.TangemPayEligibilityType
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId

View file

@ -1,11 +1,15 @@
package com.tangem.data.pay.converter
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
import arrow.core.getOrElse
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
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.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardLimit
import com.tangem.domain.models.pay.TangemPayCardLimitData
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.models.wallet.UserWalletId
import javax.inject.Inject
import javax.inject.Singleton
@ -31,29 +35,23 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
customerId = value.customerId,
)
is PaymentAccountStatusValue.IssuingCard -> PaymentAccountStatusValueDM.IssuingCard()
is PaymentAccountStatusValue.Locked -> PaymentAccountStatusValueDM.ActiveCard(
isLocked = true,
is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveAccount(
customerId = value.customerId,
cardId = value.cardId,
lastFourDigits = value.lastFourDigits,
currencyCode = value.currencyCode,
depositAddress = value.depositAddress,
isPinSet = value.isPinSet,
fiatBalance = value.fiatBalance.toDM(),
cryptoBalance = value.cryptoBalance.toDM(),
displayName = value.displayName?.value,
)
is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveCard(
isLocked = false,
customerId = value.customerId,
cardId = value.cardId,
lastFourDigits = value.lastFourDigits,
currencyCode = value.currencyCode,
depositAddress = value.depositAddress,
isPinSet = value.isPinSet,
fiatBalance = value.fiatBalance.toDM(),
cryptoBalance = value.cryptoBalance.toDM(),
displayName = value.displayName?.value,
cards = value.cards.map { card ->
PaymentAccountStatusValueDM.TangemPayCard(
id = card.id,
hasPinCode = card.hasPinCode,
displayName = card.displayName?.value,
actualDailyLimit = card.limit?.actualCardLimit?.amount,
adminDailyLimit = card.limit?.adminCardLimit?.amount,
isFrozen = card.isFrozen,
lastDigits = card.lastDigits,
)
},
)
is PaymentAccountStatusValue.Error.CardIssueFailed -> PaymentAccountStatusValueDM.CardIssueFailed(
customerId = value.customerId,
@ -76,35 +74,32 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
is PaymentAccountStatusValueDM.IssuingCard -> PaymentAccountStatusValue.IssuingCard(
source = StatusSource.CACHE,
)
is PaymentAccountStatusValueDM.ActiveCard -> if (value.isLocked) {
PaymentAccountStatusValue.Locked(
source = StatusSource.CACHE,
customerId = value.customerId,
cardId = value.cardId,
lastFourDigits = value.lastFourDigits,
currencyCode = value.currencyCode,
depositAddress = value.depositAddress,
isPinSet = value.isPinSet,
fiatBalance = value.fiatBalance.toDomain(),
cryptoBalance = value.cryptoBalance.toDomain(),
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
displayName = value.displayName?.let { CardDisplayName(it).getOrElse { null } },
)
} else {
PaymentAccountStatusValue.Loaded(
source = StatusSource.CACHE,
customerId = value.customerId,
cardId = value.cardId,
lastFourDigits = value.lastFourDigits,
currencyCode = value.currencyCode,
depositAddress = value.depositAddress,
isPinSet = value.isPinSet,
fiatBalance = value.fiatBalance.toDomain(),
cryptoBalance = value.cryptoBalance.toDomain(),
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
displayName = value.displayName?.let { CardDisplayName(it).getOrElse { null } },
)
}
is PaymentAccountStatusValueDM.ActiveAccount -> PaymentAccountStatusValue.Loaded(
source = StatusSource.CACHE,
customerId = value.customerId,
currencyCode = value.currencyCode,
depositAddress = value.depositAddress,
fiatBalance = value.fiatBalance.toDomain(),
cryptoBalance = value.cryptoBalance.toDomain(),
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
cards = value.cards.map { card ->
TangemPayCard(
id = card.id,
hasPinCode = card.hasPinCode,
displayName = card.displayName?.let { CardDisplayName(it).getOrElse { null } },
limit = TangemPayCardLimitData(
actualCardLimit = card.actualDailyLimit?.let { limit ->
TangemPayCardLimit(limit, TangemPayCardLimitPeriod.DAY)
},
adminCardLimit = card.adminDailyLimit?.let { limit ->
TangemPayCardLimit(limit, TangemPayCardLimitPeriod.DAY)
},
),
isFrozen = card.isFrozen,
lastDigits = card.lastDigits,
)
},
)
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
source = StatusSource.CACHE,
kycStatus = value.kycStatus,

View file

@ -2,6 +2,7 @@ package com.tangem.data.pay.di
import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
@ -129,6 +130,7 @@ internal interface TangemPayDataModule {
types = mapWithStringKeyTypes<PaymentAccountStatusValueDM>(),
defaultValue = emptyMap(),
),
corruptionHandler = ReplaceFileCorruptionHandler { emptyMap() },
produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") },
scope = scope,
),

View file

@ -9,6 +9,8 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardLimitData
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.model.CustomerInfo
@ -277,34 +279,28 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
customerId: String,
): PaymentAccountStatusValue {
val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId)
return when (productInstance.frozenState) {
TangemPayCardFrozenState.Frozen -> PaymentAccountStatusValue.Locked(
source = StatusSource.ACTUAL,
customerId = customerId,
cardId = productInstance.cardId,
lastFourDigits = cardInfo.lastFourDigits,
currencyCode = cardInfo.currencyCode,
depositAddress = cardInfo.depositAddress,
isPinSet = cardInfo.isPinSet,
fiatBalance = cardInfo.fiatBalance,
cryptoBalance = cardInfo.cryptoBalance,
cryptoCurrency = cryptoCurrency,
displayName = productInstance.displayName,
)
else -> PaymentAccountStatusValue.Loaded(
source = StatusSource.ACTUAL,
customerId = customerId,
cardId = productInstance.cardId,
lastFourDigits = cardInfo.lastFourDigits,
currencyCode = cardInfo.currencyCode,
depositAddress = cardInfo.depositAddress,
isPinSet = cardInfo.isPinSet,
fiatBalance = cardInfo.fiatBalance,
cryptoBalance = cardInfo.cryptoBalance,
cryptoCurrency = cryptoCurrency,
displayName = productInstance.displayName,
)
}
return PaymentAccountStatusValue.Loaded(
source = StatusSource.ACTUAL,
customerId = customerId,
currencyCode = cardInfo.currencyCode,
depositAddress = cardInfo.depositAddress,
fiatBalance = cardInfo.fiatBalance,
cryptoBalance = cardInfo.cryptoBalance,
cryptoCurrency = cryptoCurrency,
cards = listOf(
TangemPayCard(
id = productInstance.cardId,
hasPinCode = cardInfo.isPinSet,
displayName = productInstance.displayName,
limit = TangemPayCardLimitData(
actualCardLimit = productInstance.actualCardLimit,
adminCardLimit = productInstance.adminCardLimit,
),
isFrozen = productInstance.frozenState is TangemPayCardFrozenState.Frozen,
lastDigits = cardInfo.lastFourDigits,
),
),
)
}
private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatusValue {

View file

@ -15,7 +15,7 @@ import com.tangem.datasource.api.pay.models.response.OrderResponse
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.TangemPayEligibilityType
import com.tangem.domain.models.pay.TangemPayEligibilityType
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus
@ -25,8 +25,8 @@ import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
import com.tangem.domain.pay.model.TangemPayCardLimit
import com.tangem.domain.pay.model.TangemPayCardLimitPeriod
import com.tangem.domain.models.pay.TangemPayCardLimit
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.error.VisaApiError

View file

@ -8,7 +8,7 @@ import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.ReissueCardRequest
import com.tangem.datasource.api.pay.models.response.OrderResponse
import com.tangem.datasource.local.visa.TangemPayReissueCardStore
import com.tangem.domain.models.TangemPayReissueCardFee
import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayReissueOrderInfo

View file

@ -10,6 +10,7 @@ import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
@ -45,6 +46,7 @@ internal class PaymentAccountStatusesStore(
},
)
} catch (e: Exception) {
runSuspendCatching { persistenceDataStore.updateData { emptyMap() } }
TangemLogger.e("Error while loading cached payment account statuses", e)
}
}

View file

@ -78,18 +78,6 @@ class IsAccountsModeEnabledUseCaseTest {
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns true when payment account is Locked`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.Locked>())),
)
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
val actual = useCase.invoke().first()
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns false when payment account is NotCreated`() = runTest {
val statusList = createAccountStatusList(
@ -210,18 +198,6 @@ class IsAccountsModeEnabledUseCaseTest {
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns true when payment account is Locked`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.Locked>())),
)
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
val actual = useCase.invokeSync()
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns false when payment account is NotCreated`() = runTest {
val statusList = createAccountStatusList(

View file

@ -6,6 +6,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
import java.math.BigDecimal
@ -28,7 +29,6 @@ sealed class PaymentAccountStatusValue {
is UnderReview,
-> TotalFiatBalance.Loaded(amount = SerializedBigDecimal.ZERO, source = source)
is Loading -> TotalFiatBalance.Loading
is Locked -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source)
is Loaded -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source)
}
@ -41,7 +41,6 @@ sealed class PaymentAccountStatusValue {
return when (this) {
is IssuingCard -> copy(source = source)
is Loaded -> copy(source = source)
is Locked -> copy(source = source)
is UnderReview -> copy(source = source)
is Loading,
is NotCreated,
@ -84,81 +83,27 @@ sealed class PaymentAccountStatusValue {
@Serializable
data class IssuingCard(override val source: StatusSource) : PaymentAccountStatusValue()
/**
* Represents a state where the payment account is locked.
*
* @property source The source of the status information.
* @property customerId The unique identifier of the customer.
* @property cardId The unique identifier of the card.
* @property lastFourDigits The last four digits of the card number.
* @property currencyCode The code of the currency.
* @property depositAddress The address for deposits, if available.
* @property isPinSet Indicates if the PIN is set for the card.
* @property fiatBalance The fiat balance details.
* @property cryptoBalance The crypto balance details.
*/
@Serializable
data class Locked(
override val source: StatusSource,
val customerId: String,
val cardId: String,
val lastFourDigits: String,
val currencyCode: String,
val depositAddress: String?,
val isPinSet: Boolean,
val fiatBalance: FiatBalance,
val cryptoBalance: CryptoBalance,
val cryptoCurrency: CryptoCurrency.Token,
val displayName: CardDisplayName?,
) : PaymentAccountStatusValue() {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency,
value = CryptoCurrencyStatus.Loaded(
amount = cryptoBalance.balance,
fiatAmount = fiatBalance.availableBalance,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
type = NetworkAddress.Address.Type.Primary,
value = cryptoBalance.depositAddress,
),
),
sources = CryptoCurrencyStatus.Sources(),
pendingTransactions = emptySet(),
stakingBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
),
)
}
/**
* Represents a state where the payment account is successfully loaded with complete information.
*
* @property source The source of the status information.
* @property customerId The unique identifier of the customer.
* @property cardId The unique identifier of the card.
* @property lastFourDigits The last four digits of the card number.
* @property currencyCode The code of the currency.
* @property depositAddress The address for deposits, if available.
* @property isPinSet Indicates if the PIN is set for the card.
* @property fiatBalance The fiat balance details.
* @property cryptoBalance The crypto balance details.
* @property cards The list of user's cards.
*/
@Serializable
data class Loaded(
override val source: StatusSource,
val customerId: String,
val cardId: String,
val lastFourDigits: String,
val currencyCode: String,
val depositAddress: String?,
val isPinSet: Boolean,
val fiatBalance: FiatBalance,
val cryptoBalance: CryptoBalance,
val cryptoCurrency: CryptoCurrency.Token,
val displayName: CardDisplayName?,
val cards: List<TangemPayCard>,
) : PaymentAccountStatusValue() {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency,

View file

@ -0,0 +1,25 @@
package com.tangem.domain.models.pay
import com.tangem.domain.models.account.CardDisplayName
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents a Tangem Pay card linked to a payment account.
*
* @property id unique card identifier assigned by the backend.
* @property hasPinCode whether the card has a PIN code set.
* @property displayName optional human-readable name assigned to the card; `null` if not set.
* @property limit spending limit configuration for the card; `null` if not configured or not yet loaded.
* @property isFrozen whether the card is currently frozen (blocked for payments).
* @property lastDigits The last four digits of the card number.
*/
@Serializable
data class TangemPayCard(
@SerialName("id") val id: String,
@SerialName("has_pin_code") val hasPinCode: Boolean,
@SerialName("display_name") val displayName: CardDisplayName?,
@SerialName("limit") val limit: TangemPayCardLimitData?,
@SerialName("is_frozen") val isFrozen: Boolean,
@SerialName("last_digits") val lastDigits: String,
)

View file

@ -0,0 +1,49 @@
package com.tangem.domain.models.pay
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.util.Locale
@Serializable
data class TangemPayCardLimit(
@SerialName("amount") val amount: SerializedBigDecimal,
@SerialName("period") val period: TangemPayCardLimitPeriod,
)
@Serializable
enum class TangemPayCardLimitPeriod {
@SerialName("DAY")
DAY,
@SerialName("WEEK")
WEEK,
@SerialName("MONTH")
MONTH,
@SerialName("YEAR")
YEAR,
@SerialName("ALL_TIME")
ALL_TIME,
@SerialName("AUTHORIZATION")
AUTHORIZATION,
@SerialName("UNKNOWN")
UNKNOWN,
;
companion object {
fun fromString(value: String) = when (value.uppercase(Locale.US)) {
"DAY" -> DAY
"WEEK" -> WEEK
"MONTH" -> MONTH
"YEAR" -> YEAR
"ALL_TIME" -> ALL_TIME
"AUTHORIZATION" -> AUTHORIZATION
else -> UNKNOWN
}
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.models.pay
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class TangemPayCardLimitData(
@SerialName("actual_card_limit") val actualCardLimit: TangemPayCardLimit?,
@SerialName("admin_card_limit") val adminCardLimit: TangemPayCardLimit?,
)

View file

@ -1,4 +1,4 @@
package com.tangem.domain.models
package com.tangem.domain.models.pay
enum class TangemPayEligibilityType {

View file

@ -1,4 +1,4 @@
package com.tangem.domain.models
package com.tangem.domain.models.pay
import java.math.BigDecimal

View file

@ -1,5 +1,6 @@
package com.tangem.domain.pay.model
import com.tangem.domain.models.pay.TangemPayCardLimit
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus

View file

@ -1,32 +0,0 @@
package com.tangem.domain.pay.model
import java.math.BigDecimal
import java.util.Locale
data class TangemPayCardLimit(
val amount: BigDecimal,
val period: TangemPayCardLimitPeriod,
)
enum class TangemPayCardLimitPeriod {
DAY,
WEEK,
MONTH,
YEAR,
ALL_TIME,
AUTHORIZATION,
UNKNOWN,
;
companion object {
fun fromString(value: String) = when (value.uppercase(Locale.US)) {
"DAY" -> DAY
"WEEK" -> WEEK
"MONTH" -> MONTH
"YEAR" -> YEAR
"ALL_TIME" -> ALL_TIME
"AUTHORIZATION" -> AUTHORIZATION
else -> UNKNOWN
}
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.TangemPayEligibilityType
import com.tangem.domain.models.pay.TangemPayEligibilityType
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.visa.error.VisaApiError

View file

@ -3,7 +3,7 @@ package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.TangemPayReissueCardFee
import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.pay.model.TangemPayReissueOrderInfo
import com.tangem.domain.visa.error.VisaApiError

View file

@ -22,7 +22,6 @@ class GetPaymentAccountCryptoCurrencyStatusUseCase(
val accountStatus = paymentAccountStatusSupplier.invoke(userWalletId).firstOrNull() ?: return none()
val cryptoCurrencyStatus = when (val statusValue = accountStatus.value) {
is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus
is PaymentAccountStatusValue.Locked -> statusValue.cryptoCurrencyStatus
else -> return none()
}
return if (cryptoCurrencyStatus.currency == cryptoCurrency) {

View file

@ -262,7 +262,6 @@ internal class SendDestinationModel @Inject constructor(
val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: return null
val (paymentAccountAddress, currency) = when (val status = this.value) {
is PaymentAccountStatusValue.Loaded -> status.cryptoBalance.depositAddress to status.cryptoCurrency
is PaymentAccountStatusValue.Locked -> status.cryptoBalance.depositAddress to status.cryptoCurrency
else -> return null
}

View file

@ -187,7 +187,6 @@ internal class SwapInteractorImpl @AssistedInject constructor(
private fun getPaymentAccountCurrencies(accountStatus: AccountStatus.Payment): List<CryptoCurrencyStatus> {
val currencyStatus = when (val statusValue = accountStatus.value) {
is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus
is PaymentAccountStatusValue.Locked -> statusValue.cryptoCurrencyStatus
else -> return emptyList()
}

View file

@ -188,7 +188,6 @@ internal class ChooseTokenListItemConverter(
PaymentAccountStatusValue.Loading,
-> return null
is PaymentAccountStatusValue.Loaded -> status.cryptoCurrencyStatus
is PaymentAccountStatusValue.Locked -> status.cryptoCurrencyStatus
}
val account = this.account
val tokensCount = 1

View file

@ -18,12 +18,15 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayCardLimitPeriod
import com.tangem.domain.pay.model.TangemPayReissueOrderInfo
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
@ -33,22 +36,15 @@ import com.tangem.features.tangempay.components.ReissueCardListener
import com.tangem.features.tangempay.components.TangemPayCardPageComponent
import com.tangem.features.tangempay.components.ViewPinListener
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.AddToWalletBlockState
import com.tangem.features.tangempay.entity.TangemPayCardPageSetting
import com.tangem.features.tangempay.entity.TangemPayCardPageUM
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
import com.tangem.features.tangempay.entity.*
import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute
import com.tangem.features.tangempay.entity.TangemPayDailyLimitBlockState
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -57,19 +53,17 @@ import javax.inject.Inject
@ModelScoped
internal class TangemPayCardPageModel @Inject constructor(
paramsContainer: ParamsContainer,
paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val analytics: AnalyticsEventHandler,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val onboardingRepository: OnboardingRepository,
private val uiMessageSender: UiMessageSender,
private val reissueCardRepository: TangemPayReissueCardRepository,
) : Model(), ViewPinListener, ReissueCardListener, AddFundsListener {
private val params: TangemPayCardPageComponent.Params = paramsContainer.require()
private var currentFrozenState: TangemPayCardFrozenState = params.config.cardFrozenState
private val addToWalletBannerJobHolder = JobHolder()
private val addFundsJobHolder = JobHolder()
@ -78,34 +72,63 @@ internal class TangemPayCardPageModel @Inject constructor(
TangemPayCardPageUM(
onBackClick = router::pop,
dailyLimitState = TangemPayDailyLimitBlockState.Loading,
settings = persistentListOf(
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_change_pin),
onSettingClick = ::onClickChangePIN,
),
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_freeze_card),
onSettingClick = ::onClickFreezeOrUnfreezeCard,
),
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_reissue_card),
onSettingClick = ::onClickReissueCard,
),
),
settings = persistentListOf(),
),
)
val bottomSheetNavigation: SlotNavigation<TangemPayDetailsNavigation> = SlotNavigation()
// TODO v_rodionov: #[REDACTED_TASK_KEY] check reissue order state before card details are showed
init {
// TODO v_rodionov: #[REDACTED_TASK_KEY] check reissue order state before card details are showed
fetchAddToWalletBanner()
fetchCardLimit()
subscribeToCardFrozenState()
paymentAccountStatusSupplier.invoke(params.userWalletId)
.onEach { state ->
val status = state.value
if (status is PaymentAccountStatusValue.Loaded &&
status.source == StatusSource.ACTUAL &&
status.cards.isNotEmpty()
) {
val card = status.cards.first()
val limit = card.limit?.actualCardLimit?.takeIf { it.period == TangemPayCardLimitPeriod.DAY }
val dailyLimitState = if (limit != null) {
TangemPayDailyLimitBlockState.Content(
limit = limit.amount.format {
val symbol = getJavaCurrencyByCode(status.currencyCode).symbol
fiat(status.currencyCode, symbol)
},
onChangeClick = {}, // TODO v_rodionov: #[REDACTED_TASK_KEY]
)
} else {
TangemPayDailyLimitBlockState.Error
}
uiState.update { it.copy(dailyLimitState = dailyLimitState, settings = buildSettings(card)) }
} else {
// TODO v_rodionov: #[REDACTED_TASK_KEY] show error state
}
}
.launchIn(modelScope)
}
private fun onClickChangePIN() {
if (!params.config.isPinSet) {
private fun buildSettings(card: TangemPayCard): ImmutableList<TangemPayCardPageSetting> {
return persistentListOf(
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_change_pin),
onSettingClick = { onClickChangePIN(card.hasPinCode) },
),
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_freeze_card),
onSettingClick = { onClickFreezeOrUnfreezeCard(card.isFrozen) },
),
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_reissue_card),
onSettingClick = ::onClickReissueCard,
),
)
}
private fun onClickChangePIN(isPinSet: Boolean) {
if (!isPinSet) {
router.push(TangemPayDetailsInnerRoute.ChangePIN)
} else {
bottomSheetNavigation.activate(
@ -117,15 +140,13 @@ internal class TangemPayCardPageModel @Inject constructor(
}
}
private fun onClickFreezeOrUnfreezeCard() {
when (currentFrozenState) {
TangemPayCardFrozenState.Frozen -> uiMessageSender.send(
TangemPayMessagesFactory.createUnfreezeCardMessage(onUnfreezeClicked = ::unfreezeCard),
)
else -> uiMessageSender.send(
TangemPayMessagesFactory.createFreezeCardMessage(onFreezeClicked = ::freezeCard),
)
private fun onClickFreezeOrUnfreezeCard(isFrozen: Boolean) {
val message = if (isFrozen) {
TangemPayMessagesFactory.createUnfreezeCardMessage(onUnfreezeClicked = ::unfreezeCard)
} else {
TangemPayMessagesFactory.createFreezeCardMessage(onFreezeClicked = ::freezeCard)
}
uiMessageSender.send(message)
}
private fun onClickReissueCard() {
@ -242,13 +263,6 @@ internal class TangemPayCardPageModel @Inject constructor(
}
}
private fun subscribeToCardFrozenState() {
cardDetailsRepository
.cardFrozenState(params.config.cardId)
.onEach { state -> currentFrozenState = state }
.launchIn(modelScope)
}
private fun fetchAddToWalletBanner() {
modelScope.launch {
val isDone = cardDetailsRepository.isAddToWalletDone(params.userWalletId).getOrNull() == true
@ -265,39 +279,6 @@ internal class TangemPayCardPageModel @Inject constructor(
}.saveIn(addToWalletBannerJobHolder)
}
private fun fetchCardLimit() {
modelScope.launch {
onboardingRepository.getCustomerInfo(params.userWalletId)
.onRight { info ->
val productInstance = info.productInstance
val cardInfo = info.cardInfo
val actualCardLimit = productInstance?.actualCardLimit
val dailyLimitState = if (
productInstance != null &&
cardInfo != null &&
actualCardLimit?.period == TangemPayCardLimitPeriod.DAY
) {
val limit = actualCardLimit.amount.format {
val symbol = getJavaCurrencyByCode(cardInfo.currencyCode).symbol
fiat(cardInfo.currencyCode, symbol)
}
TangemPayDailyLimitBlockState.Content(
limit = limit,
onChangeClick = {}, // TODO v_rodionov: #[REDACTED_TASK_KEY]
)
} else {
TangemPayDailyLimitBlockState.Error
}
uiState.update { it.copy(dailyLimitState = dailyLimitState) }
}
.onLeft {
uiState.update { state ->
state.copy(dailyLimitState = TangemPayDailyLimitBlockState.Error)
}
}
}
}
private fun onClickAddToWallet() {
router.push(TangemPayDetailsInnerRoute.AddToWallet)
}

View file

@ -213,7 +213,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
is PaymentAccountStatusValue.IssuingCard,
is PaymentAccountStatusValue.Loaded,
is PaymentAccountStatusValue.Loading,
is PaymentAccountStatusValue.Locked,
is PaymentAccountStatusValue.UnderReview,
-> null
}

View file

@ -237,7 +237,6 @@ internal class GetWalletNotificationsFactory @Inject constructor(
is PaymentAccountStatusValue.IssuingCard,
is PaymentAccountStatusValue.Loaded,
is PaymentAccountStatusValue.Loading,
is PaymentAccountStatusValue.Locked,
is PaymentAccountStatusValue.UnderReview,
-> null
}

View file

@ -55,54 +55,37 @@ internal class TangemPayMainBlockConverter(
)
is PaymentAccountStatusValue.NotCreated -> TangemPayMainUM.Empty
is PaymentAccountStatusValue.Loading -> TangemPayMainUM.Loading
is PaymentAccountStatusValue.Locked -> TangemPayMainUM.Content(
subtitle = stringReference("*${statusValue.lastFourDigits}"),
isBalanceFlickering = statusValue.source == StatusSource.CACHE,
balance = getBalanceText(
currencyCode = statusValue.currencyCode,
balance = statusValue.fiatBalance.availableBalance,
),
balanceSubtitle = stringReference("USDC"), // TODO hardcode for now
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,
onClick = {
tangemPayClickIntents.openDetails(
value.account.userWalletId,
TangemPayDetailsConfig(
customerId = statusValue.customerId,
cardId = statusValue.cardId,
isPinSet = statusValue.isPinSet,
cardFrozenState = TangemPayCardFrozenState.Frozen,
cardNumberEnd = statusValue.lastFourDigits,
chainId = POLYGON_CHAIN_ID,
displayName = statusValue.displayName,
),
)
},
)
is PaymentAccountStatusValue.Loaded -> TangemPayMainUM.Content(
subtitle = stringReference("*${statusValue.lastFourDigits}"),
isBalanceFlickering = statusValue.source == StatusSource.CACHE,
balance = getBalanceText(
currencyCode = statusValue.currencyCode,
balance = statusValue.fiatBalance.availableBalance,
),
balanceSubtitle = stringReference("USDC"), // TODO hardcode for now
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,
onClick = {
tangemPayClickIntents.openDetails(
value.account.userWalletId,
TangemPayDetailsConfig(
customerId = statusValue.customerId,
cardId = statusValue.cardId,
isPinSet = statusValue.isPinSet,
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
cardNumberEnd = statusValue.lastFourDigits,
chainId = POLYGON_CHAIN_ID,
displayName = statusValue.displayName,
),
)
},
)
is PaymentAccountStatusValue.Loaded -> {
val card = statusValue.cards.firstOrNull() ?: return TangemPayMainUM.TemporaryUnavailable
TangemPayMainUM.Content(
subtitle = stringReference("*${card.lastDigits}"),
isBalanceFlickering = statusValue.source == StatusSource.CACHE,
balance = getBalanceText(
currencyCode = statusValue.currencyCode,
balance = statusValue.fiatBalance.availableBalance,
),
balanceSubtitle = stringReference("USDC"), // TODO hardcode for now
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,
onClick = {
tangemPayClickIntents.openDetails(
value.account.userWalletId,
TangemPayDetailsConfig(
customerId = statusValue.customerId,
cardId = card.id,
isPinSet = card.hasPinCode,
cardFrozenState = if (card.isFrozen) {
TangemPayCardFrozenState.Frozen
} else {
TangemPayCardFrozenState.Unfrozen
},
cardNumberEnd = card.lastDigits,
chainId = POLYGON_CHAIN_ID,
displayName = card.displayName,
),
)
},
)
}
}
}