Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-24 12:05:48 +05:00
parent f3c2d98214
commit f192c85914
59 changed files with 1679 additions and 432 deletions

View file

@ -8,10 +8,9 @@ import com.tangem.utils.converter.Converter
class AccountIconItemStateConverter( class AccountIconItemStateConverter(
val size: AccountIconSize = AccountIconSize.Default, val size: AccountIconSize = AccountIconSize.Default,
) : Converter<Account, CurrencyIconState.CryptoPortfolio> { ) : Converter<Account.CryptoPortfolio, CurrencyIconState.CryptoPortfolio> {
override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) { override fun convert(value: Account.CryptoPortfolio): CurrencyIconState.CryptoPortfolio = when {
is Account.CryptoPortfolio -> when {
value.icon.value == CryptoPortfolioIcon.Icon.Letter -> CurrencyIconState.CryptoPortfolio.Letter( value.icon.value == CryptoPortfolioIcon.Icon.Letter -> CurrencyIconState.CryptoPortfolio.Letter(
char = value.accountName.toUM().value, char = value.accountName.toUM().value,
color = value.icon.color.getUiColor(), color = value.icon.color.getUiColor(),
@ -25,6 +24,4 @@ class AccountIconItemStateConverter(
size = size, size = size,
) )
} }
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
} }

View file

@ -0,0 +1,98 @@
package com.tangem.common.ui.notifications
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
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.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.notifications.CloseableIconButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.ForceDarkTheme
import com.tangem.core.ui.res.TangemTheme
private const val GRADIENT_START_COLOR = 0xFF252934
private const val GRADIENT_END_COLOR = 0xFF12141E
private const val GRADIENT_OFFSET_X = 164f
private const val GRADIENT_OFFSET_Y = 39f
private const val GRADIENT_RADIUS = 82f
@Composable
fun CreatePaymentAccountNotification(
onClick: () -> Unit,
onCloseClick: () -> Unit,
@DrawableRes image: Int,
title: TextReference,
subtitle: TextReference,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(
brush = Brush.radialGradient(
colors = listOf(Color(GRADIENT_START_COLOR), Color(GRADIENT_END_COLOR)),
center = Offset(GRADIENT_OFFSET_X, GRADIENT_OFFSET_Y),
radius = GRADIENT_RADIUS,
),
)
.clickable(onClick = onClick),
) {
Image(
modifier = Modifier.size(78.dp),
painter = painterResource(id = image),
contentDescription = null,
)
Column(
modifier = Modifier
.padding(start = 78.dp, top = 12.dp, end = 12.dp, bottom = 12.dp)
.align(Alignment.CenterStart),
) {
Text(
modifier = Modifier.padding(end = TangemTheme.dimens.size32),
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.constantWhite,
)
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
CloseableIconButton(
onClick = onCloseClick,
modifier = Modifier.align(alignment = Alignment.TopEnd),
iconTint = TangemTheme.colors.icon.inactive,
)
}
}
@Preview(widthDp = 360)
@Composable
private fun CreatePaymentAccountNotification_Preview() {
ForceDarkTheme {
CreatePaymentAccountNotification(
onClick = {},
onCloseClick = {},
image = R.drawable.img_tangem_pay_visa_banner,
title = resourceReference(R.string.tangempay_onboarding_banner_title),
subtitle = resourceReference(R.string.tangempay_onboarding_banner_description),
)
}
}

View file

@ -9,7 +9,7 @@ import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.adapter.* import com.tangem.datasource.api.common.adapter.*
import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
import com.tangem.datasource.utils.SerializeNullsFactory import com.tangem.datasource.utils.SerializeNullsFactory
import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.models.scan.serialization.*
import dagger.Module import dagger.Module
@ -47,13 +47,12 @@ class MoshiModule {
.withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"), .withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"),
) )
.add( .add(
NamePolymorphicAdapterFactory.of(PaymentAccountStatusDM::class.java) NamePolymorphicAdapterFactory.of(PaymentAccountStatusValueDM::class.java)
.withSubtype(PaymentAccountStatusDM.NotCreated::class.java, "not_created") .withSubtype(PaymentAccountStatusValueDM.NotCreated::class.java, "not_created")
.withSubtype(PaymentAccountStatusDM.UnderReview::class.java, "kyc_status") .withSubtype(PaymentAccountStatusValueDM.UnderReview::class.java, "kyc_status")
.withSubtype(PaymentAccountStatusDM.IssuingCard::class.java, "issuing_card") .withSubtype(PaymentAccountStatusValueDM.IssuingCard::class.java, "issuing_card")
.withSubtype(PaymentAccountStatusDM.Locked::class.java, "locked") .withSubtype(PaymentAccountStatusValueDM.ActiveCard::class.java, "active_card")
.withSubtype(PaymentAccountStatusDM.Loaded::class.java, "balance") .withSubtype(PaymentAccountStatusValueDM.CardIssueFailed::class.java, "card_issue_failed"),
.withSubtype(PaymentAccountStatusDM.CardIssueFailed::class.java, "card_issue_failed"),
) )
.add( .add(
PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc") PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc")

View file

@ -11,43 +11,58 @@ import java.math.BigDecimal
/** /**
* Payment account status for storage in the local cache. * Payment account status for storage in the local cache.
* *
* @see [com.tangem.domain.pay.PaymentAccountStatus] * @see [com.tangem.domain.models.account.AccountStatus.Payment]
*/ */
@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER) @JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
sealed interface PaymentAccountStatusDM { sealed interface PaymentAccountStatusValueDM {
@NameLabel("not_created") @NameLabel("not_created")
data class NotCreated( data class NotCreated(
@Json(name = "not_created") val marker: Boolean = true, @Json(name = "not_created") val marker: Boolean = true,
) : PaymentAccountStatusDM ) : PaymentAccountStatusValueDM
@NameLabel("kyc_status") @NameLabel("kyc_status")
data class UnderReview( data class UnderReview(
@Json(name = "kyc_status") val kycStatus: KycStatus, @Json(name = "kyc_status") val kycStatus: KycStatus,
) : PaymentAccountStatusDM @Json(name = "customer_id") val customerId: String,
) : PaymentAccountStatusValueDM
@NameLabel("issuing_card") @NameLabel("issuing_card")
data class IssuingCard( data class IssuingCard(
@Json(name = "issuing_card") val marker: Boolean = true, @Json(name = "issuing_card") val marker: Boolean = true,
) : PaymentAccountStatusDM ) : PaymentAccountStatusValueDM
@NameLabel("locked") @NameLabel("active_card")
data class Locked( data class ActiveCard(
@Json(name = "locked") val marker: Boolean = true, @Json(name = "active_card") val isLocked: Boolean,
) : PaymentAccountStatusDM @Json(name = "customer_id") val customerId: String,
@NameLabel("balance")
data class Loaded(
@Json(name = "card_id") val cardId: String, @Json(name = "card_id") val cardId: String,
@Json(name = "last_four_digits") val lastFourDigits: String, @Json(name = "last_four_digits") val lastFourDigits: String,
@Json(name = "balance") val balance: BigDecimal,
@Json(name = "currency_code") val currencyCode: String, @Json(name = "currency_code") val currencyCode: String,
@Json(name = "deposit_address") val depositAddress: String?, @Json(name = "deposit_address") val depositAddress: String?,
@Json(name = "is_pin_set") val isPinSet: Boolean, @Json(name = "is_pin_set") val isPinSet: Boolean,
) : PaymentAccountStatusDM @Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
@Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM,
) : PaymentAccountStatusValueDM
@NameLabel("card_issue_failed") @NameLabel("card_issue_failed")
data class CardIssueFailed( data class CardIssueFailed(
@Json(name = "card_issue_failed") val marker: Boolean = true, @Json(name = "card_issue_failed") val marker: Boolean = true,
) : PaymentAccountStatusDM @Json(name = "customer_id") val customerId: String,
) : PaymentAccountStatusValueDM
@JsonClass(generateAdapter = true)
data class FiatBalanceDM(
@Json(name = "available_balance") val availableBalance: BigDecimal,
@Json(name = "currency") val currency: String,
)
@JsonClass(generateAdapter = true)
data class CryptoBalanceDM(
@Json(name = "id") val id: String,
@Json(name = "chain_id") val chainId: Long,
@Json(name = "deposit_address") val depositAddress: String,
@Json(name = "token_contract_address") val tokenContractAddress: String,
@Json(name = "balance") val balance: BigDecimal,
)
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -33,8 +33,11 @@ dependencies {
api(projects.domain.models) api(projects.domain.models)
api(projects.domain.tokens) api(projects.domain.tokens)
api(projects.domain.wallets) api(projects.domain.wallets)
api(projects.domain.visa)
// endregion // endregion
implementation(projects.features.tangempay.details.api) // Remove after TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED
// region Project - Data // region Project - Data
implementation(projects.data.common) implementation(projects.data.common)
// endregion // endregion
@ -47,6 +50,7 @@ dependencies {
// region Tangem dependencies // region Tangem dependencies
implementation(tangemDeps.card.core) implementation(tangemDeps.card.core)
implementation(tangemDeps.blockchain) implementation(tangemDeps.blockchain)
implementation(tangemDeps.hot.core)
// endregion // endregion
// region DI // region DI

View file

@ -1,10 +1,18 @@
package com.tangem.data.account.producer package com.tangem.data.account.producer
import arrow.core.Option import arrow.core.Option
import arrow.core.getOrElse
import arrow.core.none import arrow.core.none
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncStrict
import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.core.flow.FlowProducerTools
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@ -12,6 +20,7 @@ import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
/** /**
* Default implementation of [SingleAccountListProducer]. * Default implementation of [SingleAccountListProducer].
@ -27,6 +36,8 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
@Assisted val params: SingleAccountListProducer.Params, @Assisted val params: SingleAccountListProducer.Params,
override val flowProducerTools: FlowProducerTools, override val flowProducerTools: FlowProducerTools,
private val walletAccountListFlowFactory: WalletAccountListFlowFactory, private val walletAccountListFlowFactory: WalletAccountListFlowFactory,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val userWalletsListRepository: UserWalletsListRepository,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) : SingleAccountListProducer { ) : SingleAccountListProducer {
@ -34,8 +45,32 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun produce(): Flow<AccountList> { override fun produce(): Flow<AccountList> {
return walletAccountListFlowFactory.create(userWalletId = params.userWalletId) val accountListFlow: Flow<AccountList> = if (tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled) {
.flowOn(dispatchers.default) combineWithPaymentAccount()
} else {
walletAccountListFlowFactory.create(userWalletId = params.userWalletId)
}
return accountListFlow.flowOn(dispatchers.default)
}
private fun combineWithPaymentAccount(): Flow<AccountList> {
return walletAccountListFlowFactory.create(params.userWalletId)
.map { accountList ->
val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId)
if (userWallet.isPaymentAccountSupported()) {
accountList.plus(Account.Payment(params.userWalletId)).getOrElse { throwable ->
error("Can not combine account list and payment account status: $throwable")
}
} else {
accountList
}
}
}
private fun UserWallet.isPaymentAccountSupported(): Boolean = when (this) {
is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword
} }
@AssistedFactory @AssistedFactory

View file

@ -3,10 +3,12 @@ package com.tangem.data.account.producer
import com.google.common.truth.Truth import com.google.common.truth.Truth
import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.core.flow.FlowProducerTools
import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.test.core.getEmittedValues import com.tangem.test.core.getEmittedValues
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.* import io.mockk.*
@ -29,6 +31,10 @@ class DefaultSingleAccountListProducerTest {
private val userWalletId = UserWalletId("011") private val userWalletId = UserWalletId("011")
private val flowProducerTools: FlowProducerTools = mockk() private val flowProducerTools: FlowProducerTools = mockk()
private val tangemPayFeatureToggles = mockk<TangemPayFeatureToggles> {
every { this@mockk.isTangemPayAccountsRefactorEnabled } returns false
}
private val userWalletsListRepository = mockk<UserWalletsListRepository>()
private val userWallet = mockk<UserWallet> { private val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId every { this@mockk.walletId } returns userWalletId
} }
@ -38,6 +44,8 @@ class DefaultSingleAccountListProducerTest {
walletAccountListFlowFactory = walletAccountListFlowFactory, walletAccountListFlowFactory = walletAccountListFlowFactory,
dispatchers = TestingCoroutineDispatcherProvider(), dispatchers = TestingCoroutineDispatcherProvider(),
flowProducerTools = flowProducerTools, flowProducerTools = flowProducerTools,
tangemPayFeatureToggles = tangemPayFeatureToggles,
userWalletsListRepository = userWalletsListRepository,
) )
@AfterEach @AfterEach

View file

@ -1,67 +0,0 @@
package com.tangem.data.pay.converter
import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convert
import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convertBack
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.pay.PaymentAccountStatus
import com.tangem.utils.converter.TwoWayConverter
/**
* Two-way converter between [PaymentAccountStatus] and [PaymentAccountStatusDM].
*
* [convert] maps domain data model. Returns null for transient statuses that should not be persisted
* (Loading, ExposedDevice, Unavailable, NotSynced).
*
* [convertBack] maps data model domain. All restored statuses have [StatusSource.CACHE] as source.
*/
internal object PaymentAccountStatusDMConverter :
TwoWayConverter<PaymentAccountStatus, PaymentAccountStatusDM?> {
override fun convert(value: PaymentAccountStatus): PaymentAccountStatusDM? {
return when (value) {
is PaymentAccountStatus.NotCreated -> PaymentAccountStatusDM.NotCreated()
is PaymentAccountStatus.UnderReview -> PaymentAccountStatusDM.UnderReview(kycStatus = value.kycStatus)
is PaymentAccountStatus.IssuingCard -> PaymentAccountStatusDM.IssuingCard()
is PaymentAccountStatus.Locked -> PaymentAccountStatusDM.Locked()
is PaymentAccountStatus.Loaded -> PaymentAccountStatusDM.Loaded(
cardId = value.cardId,
lastFourDigits = value.lastFourDigits,
balance = value.balance,
currencyCode = value.currencyCode,
depositAddress = value.depositAddress,
isPinSet = value.isPinSet,
)
is PaymentAccountStatus.Error.CardIssueFailed -> PaymentAccountStatusDM.CardIssueFailed()
// Transient statuses are not persisted
is PaymentAccountStatus.Loading,
is PaymentAccountStatus.Error.ExposedDevice,
is PaymentAccountStatus.Error.Unavailable,
is PaymentAccountStatus.Error.NotSynced,
-> null
}
}
override fun convertBack(value: PaymentAccountStatusDM?): PaymentAccountStatus {
return when (value) {
is PaymentAccountStatusDM.CardIssueFailed -> PaymentAccountStatus.Error.CardIssueFailed
is PaymentAccountStatusDM.NotCreated -> PaymentAccountStatus.NotCreated
is PaymentAccountStatusDM.IssuingCard -> PaymentAccountStatus.IssuingCard(source = StatusSource.CACHE)
is PaymentAccountStatusDM.Locked -> PaymentAccountStatus.Locked(source = StatusSource.CACHE)
is PaymentAccountStatusDM.UnderReview -> PaymentAccountStatus.UnderReview(
source = StatusSource.CACHE,
kycStatus = value.kycStatus,
)
is PaymentAccountStatusDM.Loaded -> PaymentAccountStatus.Loaded(
source = StatusSource.CACHE,
cardId = value.cardId,
lastFourDigits = value.lastFourDigits,
balance = value.balance,
currencyCode = value.currencyCode,
depositAddress = value.depositAddress,
isPinSet = value.isPinSet,
)
null -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.CACHE)
}
}
}

View file

@ -0,0 +1,139 @@
package com.tangem.data.pay.converter
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convert
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convertBack
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.utils.converter.TwoWayConverter
/**
* Two-way converter between [PaymentAccountStatusValue] and [PaymentAccountStatusValueDM].
*
* [convert] maps domain data model. Returns null for transient statuses that should not be persisted
* (Loading, ExposedDevice, Unavailable, NotSynced).
*
* [convertBack] maps data model domain. All restored statuses have [StatusSource.CACHE] as source.
*/
internal object PaymentAccountStatusValueDMConverter :
TwoWayConverter<PaymentAccountStatusValue, PaymentAccountStatusValueDM?> {
override fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? {
return when (value) {
is PaymentAccountStatusValue.NotCreated -> PaymentAccountStatusValueDM.NotCreated()
is PaymentAccountStatusValue.UnderReview -> PaymentAccountStatusValueDM.UnderReview(
kycStatus = value.kycStatus,
customerId = value.customerId,
)
is PaymentAccountStatusValue.IssuingCard -> PaymentAccountStatusValueDM.IssuingCard()
is PaymentAccountStatusValue.Locked -> PaymentAccountStatusValueDM.ActiveCard(
isLocked = true,
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(),
)
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(),
)
is PaymentAccountStatusValue.Error.CardIssueFailed -> PaymentAccountStatusValueDM.CardIssueFailed(
customerId = value.customerId,
)
// Transient statuses are not persisted
is PaymentAccountStatusValue.Loading,
is PaymentAccountStatusValue.Error.ExposedDevice,
is PaymentAccountStatusValue.Error.Unavailable,
is PaymentAccountStatusValue.Error.NotSynced,
-> null
}
}
override fun convertBack(value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue {
return when (value) {
is PaymentAccountStatusValueDM.NotCreated -> PaymentAccountStatusValue.NotCreated
is PaymentAccountStatusValueDM.CardIssueFailed -> PaymentAccountStatusValue.Error.CardIssueFailed(
customerId = value.customerId,
)
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(),
)
} 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(),
)
}
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
source = StatusSource.CACHE,
kycStatus = value.kycStatus,
customerId = value.customerId,
)
null -> PaymentAccountStatusValue.Error.Unavailable
}
}
private fun PaymentAccountStatusValue.FiatBalance.toDM(): PaymentAccountStatusValueDM.FiatBalanceDM {
return PaymentAccountStatusValueDM.FiatBalanceDM(
availableBalance = availableBalance,
currency = currency,
)
}
private fun PaymentAccountStatusValue.CryptoBalance.toDM(): PaymentAccountStatusValueDM.CryptoBalanceDM {
return PaymentAccountStatusValueDM.CryptoBalanceDM(
id = id,
chainId = chainId,
depositAddress = depositAddress,
tokenContractAddress = tokenContractAddress,
balance = balance,
)
}
private fun PaymentAccountStatusValueDM.FiatBalanceDM.toDomain(): PaymentAccountStatusValue.FiatBalance {
return PaymentAccountStatusValue.FiatBalance(
availableBalance = availableBalance,
currency = currency,
)
}
private fun PaymentAccountStatusValueDM.CryptoBalanceDM.toDomain(): PaymentAccountStatusValue.CryptoBalance {
return PaymentAccountStatusValue.CryptoBalance(
id = id,
chainId = chainId,
depositAddress = depositAddress,
tokenContractAddress = tokenContractAddress,
balance = balance,
)
}
}

View file

@ -15,10 +15,9 @@ import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
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
@ -32,6 +31,7 @@ import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
@ -118,7 +118,7 @@ internal interface TangemPayDataModule {
persistenceDataStore = DataStoreFactory.create( persistenceDataStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer( serializer = MoshiDataStoreSerializer(
moshi = moshi, moshi = moshi,
types = mapWithStringKeyTypes<PaymentAccountStatusDM>(), types = mapWithStringKeyTypes<PaymentAccountStatusValueDM>(),
defaultValue = emptyMap(), defaultValue = emptyMap(),
), ),
produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") }, produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") },

View file

@ -2,17 +2,19 @@ package com.tangem.data.pay.flow
import arrow.core.Either import arrow.core.Either
import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.data.pay.store.PaymentAccountStatusesStore
import com.tangem.domain.core.utils.eitherOn 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.AccountStatus
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.wallet.UserWalletId
import com.tangem.domain.pay.PaymentAccountStatus
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.OrderStatus import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayCardFrozenState
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
@ -29,87 +31,101 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) : PaymentAccountStatusFetcher { ) : PaymentAccountStatusFetcher {
private val logger = TangemLogger.withTag(TAG)
override suspend fun invoke(params: PaymentAccountStatusFetcher.Params): Either<Throwable, Unit> = override suspend fun invoke(params: PaymentAccountStatusFetcher.Params): Either<Throwable, Unit> =
eitherOn(dispatchers.default) { Either.catchOn(dispatchers.default) {
TangemLogger.withTag(TAG).i("fetch: ${params.userWalletId.stringValue}") val account = Account.Payment(userWalletId = params.userWalletId)
logger.i("fetch: ${params.userWalletId.stringValue}")
if (deviceSecurity.isSecurityExposed()) { if (deviceSecurity.isSecurityExposed()) {
TangemLogger.withTag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}")
TangemLogger.withTag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") logger.i("fetch security info: xposed: ${deviceSecurity.isXposed}")
TangemLogger.withTag( logger.i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}")
TAG,
).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}")
return@eitherOn paymentAccountStatusesStore.store( return@catchOn paymentAccountStatusesStore.store(
userWalletId = params.userWalletId, userWalletId = params.userWalletId,
status = PaymentAccountStatus.Error.ExposedDevice, status = AccountStatus.Payment(
account = account,
value = PaymentAccountStatusValue.Error.ExposedDevice,
),
) )
} }
val status = onboardingRepository.hasTangemPayInWallet(userWalletId = params.userWalletId) val status = onboardingRepository.hasTangemPayInWallet(userWalletId = params.userWalletId)
.fold( .fold(
ifLeft = { error -> ifLeft = { error ->
TangemLogger.withTag( logger.e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}")
TAG,
).e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}")
when (error) { when (error) {
is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated is VisaApiError.NotPaeraCustomer -> PaymentAccountStatusValue.NotCreated
else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) else -> PaymentAccountStatusValue.Error.Unavailable
} }
}, },
ifRight = { hasTangemPay -> ifRight = { hasTangemPay ->
proceedHasTangemPayResult(userWalletId = params.userWalletId, hasTangemPay = hasTangemPay) proceedHasTangemPayResult(account = account, hasTangemPay = hasTangemPay)
}, },
) )
TangemLogger.withTag(TAG).i("invoke status ${params.userWalletId}: $status") logger.i("invoke status ${params.userWalletId}: $status")
paymentAccountStatusesStore.store(userWalletId = params.userWalletId, status = status) paymentAccountStatusesStore.store(
userWalletId = params.userWalletId,
status = AccountStatus.Payment(account = account, value = status),
)
}.onLeft {
paymentAccountStatusesStore.updateStatusSource(
userWalletId = params.userWalletId,
source = StatusSource.ONLY_CACHE,
)
} }
private suspend fun proceedHasTangemPayResult( private suspend fun proceedHasTangemPayResult(
userWalletId: UserWalletId, account: Account.Payment,
hasTangemPay: Boolean, hasTangemPay: Boolean,
): PaymentAccountStatus { ): PaymentAccountStatusValue {
TangemLogger.withTag(TAG).i("proceedHasTangemPayResult for $userWalletId hasTangemPay: $hasTangemPay") logger.i("proceedHasTangemPayResult for ${account.userWalletId} hasTangemPay: $hasTangemPay")
return if (hasTangemPay) { return if (hasTangemPay) {
fetchTangemPayAccountStatus(userWalletId = userWalletId) fetchTangemPayAccountStatus(account)
} else { } else {
PaymentAccountStatus.NotCreated PaymentAccountStatusValue.NotCreated
} }
} }
private suspend fun fetchTangemPayAccountStatus(userWalletId: UserWalletId): PaymentAccountStatus { private suspend fun fetchTangemPayAccountStatus(account: Account.Payment): PaymentAccountStatusValue {
val prevResult = paymentAccountStatusesStore.getSyncOrNull(userWalletId) val prevResult = paymentAccountStatusesStore.getSyncOrNull(account.userWalletId)
if (prevResult == null || prevResult is PaymentAccountStatus.Error) { if (prevResult == null || prevResult.value is PaymentAccountStatusValue.Error) {
paymentAccountStatusesStore.store(userWalletId = userWalletId, status = PaymentAccountStatus.Loading) paymentAccountStatusesStore.store(
userWalletId = account.userWalletId,
status = AccountStatus.Payment(account = account, value = PaymentAccountStatusValue.Loading),
)
} }
return proceedWithOrderId(userWalletId = userWalletId) return proceedWithOrderId(account = account)
} }
private suspend fun proceedWithOrderId(userWalletId: UserWalletId): PaymentAccountStatus { private suspend fun proceedWithOrderId(account: Account.Payment): PaymentAccountStatusValue {
return if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { return if (!onboardingRepository.isTangemPayInitialDataProduced(account.userWalletId)) {
PaymentAccountStatus.Error.NotSynced PaymentAccountStatusValue.Error.NotSynced
} else { } else {
val orderId = onboardingRepository.getOrderId(userWalletId) val orderId = onboardingRepository.getOrderId(account.userWalletId)
if (orderId != null) { if (orderId != null) {
proceedWithOrderId(userWalletId = userWalletId, orderId = orderId) proceedWithOrderId(account = account, orderId = orderId)
} else { } else {
proceedWithoutOrder(userWalletId = userWalletId) proceedWithoutOrder(account = account)
} }
} }
} }
private suspend fun proceedWithoutOrder(userWalletId: UserWalletId): PaymentAccountStatus { private suspend fun proceedWithoutOrder(account: Account.Payment): PaymentAccountStatusValue {
return onboardingRepository.getCustomerInfo(userWalletId).fold( return onboardingRepository.getCustomerInfo(account.userWalletId).fold(
ifLeft = { error -> ifLeft = { error ->
TangemLogger.withTag(TAG).e("proceedWithoutOrder $userWalletId error: $error") logger.e("proceedWithoutOrder ${account.userWalletId} error: $error")
error.mapToPaymentAccountStatus() error.mapToPaymentAccountStatus()
}, },
ifRight = { customerInfo -> ifRight = { customerInfo ->
TangemLogger.withTag(TAG).i("proceedWithoutOrder data customerInfo $userWalletId") logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}")
val status = customerInfo.mapToPaymentAccountStatus() val status = customerInfo.mapToPaymentAccountStatus()
if (customerInfo.productInstance == null) { if (status is PaymentAccountStatusValue.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) {
onboardingRepository.createOrder(userWalletId) // If order id wasn't saved -> start order creation and get customer info
onboardingRepository.createOrder(account.userWalletId)
.onLeft { TangemLogger.withTag(TAG).e("createOrder failed: $it") } .onLeft { TangemLogger.withTag(TAG).e("createOrder failed: $it") }
} }
status status
@ -117,63 +133,94 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
) )
} }
private suspend fun proceedWithOrderId(userWalletId: UserWalletId, orderId: String): PaymentAccountStatus { private suspend fun proceedWithOrderId(account: Account.Payment, orderId: String): PaymentAccountStatusValue {
return customerOrderRepository.getOrderData(userWalletId, orderId = orderId).fold( return customerOrderRepository.getOrderData(userWalletId = account.userWalletId, orderId = orderId).fold(
ifLeft = { error -> ifLeft = { error ->
TangemLogger.withTag(TAG).e("proceedWithOrderId $userWalletId orderId: $orderId error: $error") logger.e("proceedWithOrderId ${account.userWalletId} orderId: $orderId error: $error")
error.mapToPaymentAccountStatus() error.mapToPaymentAccountStatus()
}, },
ifRight = { orderData -> ifRight = { orderData ->
TangemLogger.withTag(TAG).i("proceedWithOrderId $userWalletId: $orderId status: ${orderData.status}") logger.i("proceedWithOrderId $account.userWalletId: $orderId status: ${orderData.status}")
when (orderData.status) { when (orderData.status) {
// Kyc is passed and user waits for order creation -> no need to get customer info // Kyc is passed and user waits for order creation -> no need to get customer info
OrderStatus.NEW, OrderStatus.NEW,
OrderStatus.PROCESSING, OrderStatus.PROCESSING,
-> PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) -> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
OrderStatus.CANCELED -> { OrderStatus.CANCELED -> {
PaymentAccountStatus.Error.CardIssueFailed PaymentAccountStatusValue.Error.CardIssueFailed(customerId = orderData.customerId)
} }
OrderStatus.COMPLETED -> { OrderStatus.COMPLETED -> {
// Order was completed -> clear order id and get customer info // Order was completed -> clear order id and get customer info
onboardingRepository.clearOrderId(userWalletId) onboardingRepository.clearOrderId(account.userWalletId)
onboardingRepository.getCustomerInfo(userWalletId = userWalletId) onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId)
.fold( .fold(
ifLeft = { it.mapToPaymentAccountStatus() }, ifLeft = { it.mapToPaymentAccountStatus() },
ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() }, ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() },
) )
} }
OrderStatus.UNKNOWN -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) OrderStatus.UNKNOWN -> PaymentAccountStatusValue.Error.Unavailable
} }
}, },
) )
} }
private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatus { private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatusValue {
val cardInfo = this.cardInfo val cardInfo = this.cardInfo
val productInstance = this.productInstance val productInstance = this.productInstance
return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) { return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) {
PaymentAccountStatus.UnderReview(source = StatusSource.ACTUAL, kycStatus = kycStatus) PaymentAccountStatusValue.UnderReview(
} else if (cardInfo != null && productInstance != null) {
PaymentAccountStatus.Loaded(
source = StatusSource.ACTUAL, source = StatusSource.ACTUAL,
cardId = productInstance.cardId, kycStatus = kycStatus,
lastFourDigits = cardInfo.lastFourDigits, customerId = requireNotNull(customerId) { "CustomerId must not be null" },
balance = cardInfo.balance, )
currencyCode = cardInfo.currencyCode, } else if (cardInfo != null && productInstance != null && !customerId.isNullOrEmpty()) {
depositAddress = cardInfo.depositAddress, convertToContentState(
isPinSet = cardInfo.isPinSet, productInstance = productInstance,
cardInfo = cardInfo,
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
) )
} else { } else {
PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
} }
} }
private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatus { private fun convertToContentState(
productInstance: CustomerInfo.ProductInstance,
cardInfo: CustomerInfo.CardInfo,
customerId: String,
): PaymentAccountStatusValue {
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,
)
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,
)
}
}
private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatusValue {
return when (this) { return when (this) {
is VisaApiError.RefreshTokenExpired -> PaymentAccountStatus.Error.NotSynced is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced
is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated is VisaApiError.NotPaeraCustomer -> PaymentAccountStatusValue.NotCreated
else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) else -> PaymentAccountStatusValue.Error.Unavailable
} }
} }
} }

View file

@ -4,8 +4,9 @@ import arrow.core.Option
import arrow.core.some import arrow.core.some
import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.data.pay.store.PaymentAccountStatusesStore
import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.core.flow.FlowProducerTools
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.account.Account
import com.tangem.domain.pay.PaymentAccountStatus import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted import dagger.assisted.Assisted
@ -21,12 +22,15 @@ internal class DefaultPaymentAccountStatusProducer @AssistedInject constructor(
private val paymentAccountStatusesStore: PaymentAccountStatusesStore, private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) : PaymentAccountStatusProducer { ) : PaymentAccountStatusProducer {
override val fallback: Option<PaymentAccountStatus>
get() = PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL).some()
override fun produce(): Flow<PaymentAccountStatus> { private val account = Account.Payment(userWalletId = params.userWalletId)
override val fallback: Option<AccountStatus.Payment>
get() = AccountStatus.Payment(account = account, value = PaymentAccountStatusValue.Error.Unavailable).some()
override fun produce(): Flow<AccountStatus.Payment> {
return paymentAccountStatusesStore.get(userWalletId = params.userWalletId) return paymentAccountStatusesStore.get(userWalletId = params.userWalletId)
.onEmpty { emit(value = PaymentAccountStatus.NotCreated) } .onEmpty { emit(value = AccountStatus.Payment(account, PaymentAccountStatusValue.NotCreated)) }
.flowOn(dispatchers.default) .flowOn(dispatchers.default)
} }

View file

@ -13,6 +13,7 @@ import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.TangemPayStorage 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.TangemPayEligibilityType import com.tangem.domain.models.TangemPayEligibilityType
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.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
@ -139,6 +140,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
?: error("no userWallet found") ?: error("no userWallet found")
} }
@Suppress("ComplexCondition")
private suspend fun getCustomerInfo( private suspend fun getCustomerInfo(
userWalletId: UserWalletId, userWalletId: UserWalletId,
response: CustomerMeResponse.Result?, response: CustomerMeResponse.Result?,
@ -148,14 +150,26 @@ internal class DefaultOnboardingRepository @Inject constructor(
val card = response?.card val card = response?.card
val fiatBalance = response?.balance?.fiat val fiatBalance = response?.balance?.fiat
val cryptoBalance = response?.balance?.crypto
val paymentAccount = response?.paymentAccount val paymentAccount = response?.paymentAccount
val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null) { val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null && cryptoBalance != null) {
CardInfo( CardInfo(
lastFourDigits = card.cardNumberEnd, lastFourDigits = card.cardNumberEnd,
balance = fiatBalance.availableBalance, balance = fiatBalance.availableBalance,
currencyCode = fiatBalance.currency, currencyCode = fiatBalance.currency,
depositAddress = response.depositAddress, depositAddress = response.depositAddress,
isPinSet = response.card?.isPinSet == true, isPinSet = response.card?.isPinSet == true,
fiatBalance = PaymentAccountStatusValue.FiatBalance(
availableBalance = fiatBalance.availableBalance,
currency = fiatBalance.currency,
),
cryptoBalance = PaymentAccountStatusValue.CryptoBalance(
id = cryptoBalance.id,
chainId = cryptoBalance.chainId.toLong(),
depositAddress = cryptoBalance.depositAddress.orEmpty(),
tokenContractAddress = cryptoBalance.tokenContractAddress,
balance = cryptoBalance.balance,
),
) )
} else { } else {
null null
@ -167,7 +181,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
} }
cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState) cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState)
ProductInstance(id = instance.id, cardId = instance.cardId) ProductInstance(id = instance.id, cardId = instance.cardId, frozenState = cardFrozenState)
} }
return CustomerInfo( return CustomerInfo(
customerId = response?.id, customerId = response?.id,

View file

@ -1,11 +1,14 @@
package com.tangem.data.pay.store package com.tangem.data.pay.store
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter
import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.PaymentAccountStatus
import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
@ -14,8 +17,8 @@ import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
internal typealias WalletIdWithPaymentStatus = Map<String, PaymentAccountStatus> internal typealias WalletIdWithPaymentStatus = Map<String, AccountStatus.Payment>
internal typealias WalletIdWithPaymentStatusDM = Map<String, PaymentAccountStatusDM> internal typealias WalletIdWithPaymentStatusDM = Map<String, PaymentAccountStatusValueDM>
/** /**
* Store for payment account statuses with dual storage (runtime + persistence). * Store for payment account statuses with dual storage (runtime + persistence).
@ -26,7 +29,7 @@ internal typealias WalletIdWithPaymentStatusDM = Map<String, PaymentAccountStatu
internal class PaymentAccountStatusesStore( internal class PaymentAccountStatusesStore(
private val runtimeStore: RuntimeSharedStore<WalletIdWithPaymentStatus>, private val runtimeStore: RuntimeSharedStore<WalletIdWithPaymentStatus>,
private val persistenceDataStore: DataStore<WalletIdWithPaymentStatusDM>, private val persistenceDataStore: DataStore<WalletIdWithPaymentStatusDM>,
private val scope: AppCoroutineScope, scope: AppCoroutineScope,
) { ) {
init { init {
@ -34,8 +37,10 @@ internal class PaymentAccountStatusesStore(
try { try {
val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch
runtimeStore.store( runtimeStore.store(
value = cachedStatuses.mapValues { (_, statusDM) -> value = cachedStatuses.mapValues { (rawUserWalletId, statusDM) ->
PaymentAccountStatusDMConverter.convertBack(statusDM) val account = Account.Payment(userWalletId = UserWalletId(rawUserWalletId))
val statusValue = PaymentAccountStatusValueDMConverter.convertBack(value = statusDM)
AccountStatus.Payment(account = account, value = statusValue)
}, },
) )
} catch (e: Exception) { } catch (e: Exception) {
@ -44,18 +49,28 @@ internal class PaymentAccountStatusesStore(
} }
} }
fun get(userWalletId: UserWalletId): Flow<PaymentAccountStatus> { fun get(userWalletId: UserWalletId): Flow<AccountStatus.Payment> {
return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] } return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] }
} }
suspend fun getSyncOrNull(userWalletId: UserWalletId): PaymentAccountStatus? { suspend fun getSyncOrNull(userWalletId: UserWalletId): AccountStatus.Payment? {
return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue) return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue)
} }
suspend fun store(userWalletId: UserWalletId, status: PaymentAccountStatus) { suspend fun updateStatusSource(userWalletId: UserWalletId, source: StatusSource) {
runtimeStore.update(emptyMap()) { stored ->
stored.toMutableMap().apply {
val paymentAccountStatus = this[userWalletId.stringValue] ?: return@update stored
val newValue = paymentAccountStatus.copy(value = paymentAccountStatus.value.copySealed(source = source))
put(key = userWalletId.stringValue, value = newValue)
}
}
}
suspend fun store(userWalletId: UserWalletId, status: AccountStatus.Payment) {
coroutineScope { coroutineScope {
launch { storeInRuntime(userWalletId = userWalletId, status = status) } launch { storeInRuntime(userWalletId = userWalletId, status = status) }
launch { storeInPersistence(userWalletId = userWalletId, status = status) } launch { storeInPersistence(userWalletId = userWalletId, status = status.value) }
} }
} }
@ -63,7 +78,7 @@ internal class PaymentAccountStatusesStore(
return runtimeStore.getSyncOrDefault(emptyMap()).containsKey(userWalletId.stringValue) return runtimeStore.getSyncOrDefault(emptyMap()).containsKey(userWalletId.stringValue)
} }
private suspend fun storeInRuntime(userWalletId: UserWalletId, status: PaymentAccountStatus) { private suspend fun storeInRuntime(userWalletId: UserWalletId, status: AccountStatus.Payment) {
runtimeStore.update(default = emptyMap()) { stored -> runtimeStore.update(default = emptyMap()) { stored ->
stored.toMutableMap().apply { stored.toMutableMap().apply {
put(key = userWalletId.stringValue, value = status) put(key = userWalletId.stringValue, value = status)
@ -71,8 +86,8 @@ internal class PaymentAccountStatusesStore(
} }
} }
private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatus) { private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatusValue) {
val statusDM = PaymentAccountStatusDMConverter.convert(value = status) ?: return val statusDM = PaymentAccountStatusValueDMConverter.convert(value = status) ?: return
persistenceDataStore.updateData { storedStatuses -> persistenceDataStore.updateData { storedStatuses ->
storedStatuses.toMutableMap().apply { storedStatuses.toMutableMap().apply {
put(key = userWalletId.stringValue, value = statusDM) put(key = userWalletId.stringValue, value = statusDM)

View file

@ -108,11 +108,9 @@ data class AccountList private constructor(
} }
fun flattenMapCurrencies(): Map<AccountCurrencyId, CryptoCurrency> = buildMap { fun flattenMapCurrencies(): Map<AccountCurrencyId, CryptoCurrency> = buildMap {
accounts.forEach { acc -> accounts
val account = when (acc) { .filterIsInstance<Account.CryptoPortfolio>()
is Account.CryptoPortfolio -> acc .forEach { account ->
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
account.cryptoCurrencies.forEach { currency -> account.cryptoCurrencies.forEach { currency ->
val key = account.accountId to currency.id val key = account.accountId to currency.id
put(key, currency) put(key, currency)

View file

@ -28,6 +28,7 @@ dependencies {
api(projects.domain.staking) api(projects.domain.staking)
api(projects.domain.tokens) api(projects.domain.tokens)
api(projects.domain.tokens.models) api(projects.domain.tokens.models)
api(projects.domain.visa)
api(projects.domain.walletManager) api(projects.domain.walletManager)
api(projects.domain.wallets) api(projects.domain.wallets)
@ -39,6 +40,8 @@ dependencies {
implementation(deps.kotlin.serialization) implementation(deps.kotlin.serialization)
implementation(tangemDeps.blockchain) implementation(tangemDeps.blockchain)
implementation(tangemDeps.card.core)
implementation(tangemDeps.hot.core)
// region DI // region DI
implementation(deps.hilt.android) implementation(deps.hilt.android)

View file

@ -3,6 +3,7 @@ package com.tangem.domain.account.status.producer
import arrow.core.Option import arrow.core.Option
import arrow.core.none import arrow.core.none
import arrow.core.toOption import arrow.core.toOption
import com.tangem.common.card.FirmwareVersion
import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.domain.account.models.AccountCurrencyId import com.tangem.domain.account.models.AccountCurrencyId
import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.AccountList
@ -33,6 +34,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.networks.repository.NetworksRepository
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.quotes.multi.MultiQuoteStatusSupplier import com.tangem.domain.quotes.multi.MultiQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
@ -42,6 +44,7 @@ import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory
import com.tangem.domain.tokens.operations.PriceChangeCalculator import com.tangem.domain.tokens.operations.PriceChangeCalculator
import com.tangem.domain.tokens.operations.TokenListFactory import com.tangem.domain.tokens.operations.TokenListFactory
import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@ -69,6 +72,7 @@ import java.math.BigDecimal
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
// TODO: Move to :data:account:status [REDACTED_JIRA]
@Suppress("LongParameterList") @Suppress("LongParameterList")
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor( internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor(
@ -76,6 +80,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
override val flowProducerTools: FlowProducerTools, override val flowProducerTools: FlowProducerTools,
private val userWalletsListRepository: UserWalletsListRepository, private val userWalletsListRepository: UserWalletsListRepository,
private val singleAccountListSupplier: SingleAccountListSupplier, private val singleAccountListSupplier: SingleAccountListSupplier,
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
private val networksRepository: NetworksRepository, private val networksRepository: NetworksRepository,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
private val networkStatusSupplier: MultiNetworkStatusSupplier, private val networkStatusSupplier: MultiNetworkStatusSupplier,
@ -116,19 +121,39 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
flattenCurrency = flattenCurrency, flattenCurrency = flattenCurrency,
) )
combine( if (userWallet.isPaymentAccountSupported()) {
combineWithPaymentAccount(
accountListFlow = accountListFlow,
cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow,
paymentAccountStatusFlow = paymentAccountStatusSupplier.invoke(userWalletId = params.userWalletId),
)
} else {
combineWithoutPaymentAccount(
accountListFlow = accountListFlow,
cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow,
)
}
.collect { accountStatusList -> channel.send(accountStatusList) }
}
private fun combineWithPaymentAccount(
accountListFlow: StateFlow<AccountList>,
cryptoCurrencyStatusFlow: Flow<Map<AccountCurrencyId, CryptoCurrencyStatus>>,
paymentAccountStatusFlow: Flow<AccountStatus.Payment>,
): Flow<AccountStatusList> {
return combine(
flow = accountListFlow, flow = accountListFlow,
flow2 = cryptoCurrencyStatusFlow, flow2 = cryptoCurrencyStatusFlow,
transform = { accountList, currencyStatusMap -> flow3 = paymentAccountStatusFlow,
val accountStatuses: List<AccountStatus.CryptoPortfolio> = accountList.accounts.map { acc -> transform = { accountList, currencyStatusMap, paymentAccountStatus ->
val account: Account.CryptoPortfolio = when (acc) { val accountStatuses = accountList.accounts.map { account ->
is Account.CryptoPortfolio -> acc when (account) {
is Account.Payment -> TODO("[REDACTED_JIRA]") is Account.Payment -> paymentAccountStatus
} is Account.CryptoPortfolio -> if (account.cryptoCurrencies.isEmpty()) {
if (account.cryptoCurrencies.isEmpty()) {
account.toEmptyAccountStatus() account.toEmptyAccountStatus()
} else { } else {
val statuses: List<CryptoCurrencyStatus> = account.cryptoCurrencies.map { currency -> val statuses: List<CryptoCurrencyStatus> =
account.cryptoCurrencies.map { currency ->
val acId = account.accountId to currency.id val acId = account.accountId to currency.id
currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus() currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus()
} }
@ -143,6 +168,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
) )
} }
} }
}
val balances = accountStatuses.flattenTotalFiatBalance() val balances = accountStatuses.flattenTotalFiatBalance()
AccountStatusList( AccountStatusList(
@ -156,7 +182,58 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
) )
}, },
) )
.collect { accountStatusList -> channel.send(accountStatusList) } }
private fun combineWithoutPaymentAccount(
accountListFlow: StateFlow<AccountList>,
cryptoCurrencyStatusFlow: Flow<Map<AccountCurrencyId, CryptoCurrencyStatus>>,
): Flow<AccountStatusList> {
return combine(
flow = accountListFlow,
flow2 = cryptoCurrencyStatusFlow,
transform = { accountList, currencyStatusMap ->
val accountStatuses = accountList.accounts
.filterIsInstance<Account.CryptoPortfolio>()
.map { account ->
when (account) {
is Account.CryptoPortfolio -> if (account.cryptoCurrencies.isEmpty()) {
account.toEmptyAccountStatus()
} else {
val statuses: List<CryptoCurrencyStatus> =
account.cryptoCurrencies.map { currency ->
val acId = account.accountId to currency.id
currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus()
}
AccountStatus.CryptoPortfolio(
account = account,
tokenList = TokenListFactory.create(
statuses = statuses,
groupType = accountList.groupType,
sortType = accountList.sortType,
),
priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses),
)
}
}
}
val balances = accountStatuses.flattenTotalFiatBalance()
AccountStatusList(
userWalletId = accountList.userWalletId,
accountStatuses = accountStatuses,
totalAccounts = accountList.totalAccounts,
totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances),
totalArchivedAccounts = accountList.totalArchivedAccounts,
sortType = accountList.sortType,
groupType = accountList.groupType,
)
},
)
}
private fun UserWallet.isPaymentAccountSupported(): Boolean = when (this) {
is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword
} }
private fun ProducerScope<AccountStatusList>.flattenCurrencyStatusFlow( private fun ProducerScope<AccountStatusList>.flattenCurrencyStatusFlow(
@ -278,7 +355,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
return map { accountStatus -> return map { accountStatus ->
when (accountStatus) { when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance
is AccountStatus.Payment -> accountStatus.totalFiatBalance is AccountStatus.Payment -> accountStatus.value.totalFiatBalance
} }
} }
} }

View file

@ -178,12 +178,15 @@ sealed interface Account {
@Serializable @Serializable
data class Payment( data class Payment(
override val accountId: AccountId, override val accountId: AccountId,
override val accountName: AccountName,
val cryptoCurrencies: List<CryptoCurrency>,
) : Account { ) : Account {
override val accountName: AccountName.Custom = AccountName.Custom("Payment").getOrElse {
error("Can not create account name for Payment account with userWalletId = ${accountId.userWalletId}")
}
init { companion object {
error("Not yet implemented") operator fun invoke(userWalletId: UserWalletId): Payment {
return Payment(accountId = AccountId.forPaymentAccount(userWalletId = userWalletId))
}
} }
} }
} }

View file

@ -1,7 +1,6 @@
package com.tangem.domain.models.account package com.tangem.domain.models.account
import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.quote.PriceChange import com.tangem.domain.models.quote.PriceChange
import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.tokenlist.TokenList
@ -43,7 +42,7 @@ sealed interface AccountStatus {
@Serializable @Serializable
data class Payment( data class Payment(
override val account: Account.Payment, override val account: Account.Payment,
val totalFiatBalance: TotalFiatBalance, val value: PaymentAccountStatusValue,
) : AccountStatus ) : AccountStatus
} }

View file

@ -0,0 +1,193 @@
package com.tangem.domain.models.account
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
/**
* Represents the various states a payment account can have, encapsulating different information based on the state.
*
* @property source The source of the status information.
*/
@Serializable
sealed class PaymentAccountStatusValue {
abstract val source: StatusSource
/** The total fiat balance associated with this status. */
val totalFiatBalance: TotalFiatBalance
get() = when (this) {
is Error,
is IssuingCard,
is NotCreated,
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)
}
/**
* Copies the status with a new [source].
*
* @param source The new source of the status information.
*/
fun copySealed(source: StatusSource): 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,
is Error,
-> this
}
}
/** Represents the Loading state of a payment account, typically while fetching its details. */
@Serializable
data object Loading : PaymentAccountStatusValue() {
override val source: StatusSource = StatusSource.ACTUAL
}
/** Represents a state where the payment account has not been created yet. */
@Serializable
data object NotCreated : PaymentAccountStatusValue() {
override val source: StatusSource = StatusSource.ACTUAL
}
/**
* Represents a state where the payment account is under review (KYC).
*
* @property source The source of the status information.
* @property kycStatus The current KYC status.
* @property customerId The unique identifier of the customer.
*/
@Serializable
data class UnderReview(
override val source: StatusSource,
val kycStatus: KycStatus,
val customerId: String,
) : PaymentAccountStatusValue()
/**
* Represents a state where the card for the payment account is being issued.
*
* @property source The source of the status information.
*/
@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,
) : PaymentAccountStatusValue()
/**
* 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.
*/
@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,
) : PaymentAccountStatusValue()
/** Represents an error state for the payment account status. */
@Serializable
sealed class Error : PaymentAccountStatusValue() {
/** Error state indicating the device is exposed. */
@Serializable
data object ExposedDevice : Error() {
override val source: StatusSource = StatusSource.ACTUAL
}
/** Error state indicating the account is unavailable. */
@Serializable
data object Unavailable : Error() {
override val source: StatusSource = StatusSource.ACTUAL
}
/** Error state indicating the account data is not synced. */
@Serializable
data object NotSynced : Error() {
override val source: StatusSource = StatusSource.ACTUAL
}
/**
* Error state indicating that card issuance failed.
*
* @property customerId The unique identifier of the customer.
*/
@Serializable
data class CardIssueFailed(val customerId: String) : Error() {
override val source: StatusSource = StatusSource.ACTUAL
}
}
/**
* Represents the fiat balance of the payment account.
*
* @property availableBalance The amount of available balance in fiat.
* @property currency The currency of the balance.
*/
@Serializable
data class FiatBalance(val availableBalance: SerializedBigDecimal, val currency: String)
/**
* Represents the crypto balance of the payment account.
*
* @property id The unique identifier of the crypto asset.
* @property chainId The identifier of the blockchain network.
* @property depositAddress The address for deposits.
* @property tokenContractAddress The contract address of the token.
* @property balance The amount of the crypto balance.
*/
@Serializable
data class CryptoBalance(
val id: String,
val chainId: Long,
val depositAddress: String,
val tokenContractAddress: String,
val balance: SerializedBigDecimal,
)
}

View file

@ -1,66 +0,0 @@
package com.tangem.domain.pay
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
@Serializable
sealed class PaymentAccountStatus {
abstract val source: StatusSource
@Serializable
data object Loading : PaymentAccountStatus() {
override val source: StatusSource = StatusSource.ACTUAL
}
@Serializable
data object NotCreated : PaymentAccountStatus() {
override val source: StatusSource = StatusSource.ACTUAL
}
@Serializable
data class UnderReview(
override val source: StatusSource,
val kycStatus: KycStatus,
) : PaymentAccountStatus()
@Serializable
data class IssuingCard(override val source: StatusSource) : PaymentAccountStatus()
@Serializable
data class Locked(override val source: StatusSource) : PaymentAccountStatus()
@Serializable
data class Loaded(
override val source: StatusSource,
val cardId: String,
val lastFourDigits: String,
val balance: SerializedBigDecimal,
val currencyCode: String,
val depositAddress: String?,
val isPinSet: Boolean,
) : PaymentAccountStatus()
@Serializable
sealed class Error : PaymentAccountStatus() {
@Serializable
data object ExposedDevice : Error() {
override val source: StatusSource = StatusSource.ACTUAL
}
@Serializable
data class Unavailable(override val source: StatusSource) : Error()
@Serializable
data object NotSynced : Error() {
override val source: StatusSource = StatusSource.ACTUAL
}
@Serializable
data object CardIssueFailed : Error() {
override val source: StatusSource = StatusSource.ACTUAL
}
}
}

View file

@ -1,10 +1,10 @@
package com.tangem.domain.pay.flow package com.tangem.domain.pay.flow
import com.tangem.domain.core.flow.FlowProducer import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.PaymentAccountStatus
interface PaymentAccountStatusProducer : FlowProducer<PaymentAccountStatus> { interface PaymentAccountStatusProducer : FlowProducer<AccountStatus.Payment> {
data class Params(val userWalletId: UserWalletId) data class Params(val userWalletId: UserWalletId)
interface Factory : FlowProducer.Factory<Params, PaymentAccountStatusProducer> interface Factory : FlowProducer.Factory<Params, PaymentAccountStatusProducer>

View file

@ -1,10 +1,18 @@
package com.tangem.domain.pay.flow package com.tangem.domain.pay.flow
import com.tangem.domain.core.flow.FlowCachingSupplier import com.tangem.domain.core.flow.FlowCachingSupplier
import com.tangem.domain.pay.PaymentAccountStatus import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
@Suppress("UnnecessaryAbstractClass") @Suppress("UnnecessaryAbstractClass")
abstract class PaymentAccountStatusSupplier( abstract class PaymentAccountStatusSupplier(
override val factory: PaymentAccountStatusProducer.Factory, override val factory: PaymentAccountStatusProducer.Factory,
override val keyCreator: (PaymentAccountStatusProducer.Params) -> String, override val keyCreator: (PaymentAccountStatusProducer.Params) -> String,
) : FlowCachingSupplier<PaymentAccountStatusProducer, PaymentAccountStatusProducer.Params, PaymentAccountStatus>() ) : FlowCachingSupplier<PaymentAccountStatusProducer, PaymentAccountStatusProducer.Params, AccountStatus.Payment>() {
operator fun invoke(userWalletId: UserWalletId): Flow<AccountStatus.Payment> {
val params = PaymentAccountStatusProducer.Params(userWalletId)
return this.invoke(params)
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.domain.pay.model package com.tangem.domain.pay.model
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.visa.model.TangemPayCardFrozenState
import java.math.BigDecimal import java.math.BigDecimal
sealed class MainCustomerInfoContentState { sealed class MainCustomerInfoContentState {
@ -25,6 +27,7 @@ data class CustomerInfo(
data class ProductInstance( data class ProductInstance(
val id: String, val id: String,
val cardId: String, val cardId: String,
val frozenState: TangemPayCardFrozenState,
) )
data class CardInfo( data class CardInfo(
@ -33,5 +36,7 @@ data class CustomerInfo(
val currencyCode: String, val currencyCode: String,
val depositAddress: String?, val depositAddress: String?,
val isPinSet: Boolean, val isPinSet: Boolean,
val fiatBalance: PaymentAccountStatusValue.FiatBalance,
val cryptoBalance: PaymentAccountStatusValue.CryptoBalance,
) )
} }

View file

@ -15,8 +15,6 @@ import com.tangem.security.isSecurityExposed
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
private const val TAG = "TangemPayMainScreenCustomerInfoUseCase"
class TangemPayMainScreenCustomerInfoUseCase( class TangemPayMainScreenCustomerInfoUseCase(
private val onboardingRepository: OnboardingRepository, private val onboardingRepository: OnboardingRepository,
private val customerOrderRepository: CustomerOrderRepository, private val customerOrderRepository: CustomerOrderRepository,
@ -27,15 +25,15 @@ class TangemPayMainScreenCustomerInfoUseCase(
val state: StateFlow<Map<UserWalletId, Either<TangemPayCustomerInfoError, MainCustomerInfoContentState>>> val state: StateFlow<Map<UserWalletId, Either<TangemPayCustomerInfoError, MainCustomerInfoContentState>>>
field = MutableStateFlow(value = mapOf()) field = MutableStateFlow(value = mapOf())
private val logger = TangemLogger.withTag("TangemPayMainScreenCustomerInfoUseCase")
suspend fun fetch(userWalletId: UserWalletId) { suspend fun fetch(userWalletId: UserWalletId) {
TangemLogger.withTag(TAG).i("fetch: ${userWalletId.stringValue}") logger.i("fetch: ${userWalletId.stringValue}")
if (deviceSecurity.isSecurityExposed()) { if (deviceSecurity.isSecurityExposed()) {
TangemLogger.withTag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}")
TangemLogger.withTag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") logger.i("fetch security info: xposed: ${deviceSecurity.isXposed}")
TangemLogger.withTag( logger.i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}")
TAG,
).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}")
updateState(userWalletId = userWalletId, either = TangemPayCustomerInfoError.ExposedDeviceError.left()) updateState(userWalletId = userWalletId, either = TangemPayCustomerInfoError.ExposedDeviceError.left())
return // fast exit return // fast exit
@ -44,9 +42,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
onboardingRepository.hasTangemPayInWallet(userWalletId) onboardingRepository.hasTangemPayInWallet(userWalletId)
.fold( .fold(
ifLeft = { error -> ifLeft = { error ->
TangemLogger.withTag( logger.e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}")
TAG,
).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}")
if (error is VisaApiError.NotPaeraCustomer) { if (error is VisaApiError.NotPaeraCustomer) {
showOnboardingBannerIfEligible(userWalletId) showOnboardingBannerIfEligible(userWalletId)
} else { } else {
@ -54,7 +50,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
} }
}, },
ifRight = { hasTangemPay -> ifRight = { hasTangemPay ->
TangemLogger.withTag(TAG).i("checkCustomerWallet for $userWalletId: $hasTangemPay") logger.i("checkCustomerWallet for $userWalletId: $hasTangemPay")
if (hasTangemPay) { if (hasTangemPay) {
val oldResult = state.value[userWalletId] val oldResult = state.value[userWalletId]
if (oldResult == null) { if (oldResult == null) {
@ -129,11 +125,11 @@ class TangemPayMainScreenCustomerInfoUseCase(
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> { ): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
return onboardingRepository.getCustomerInfo(userWalletId) return onboardingRepository.getCustomerInfo(userWalletId)
.mapLeft { error -> .mapLeft { error ->
TangemLogger.withTag(TAG).e("mapErrorForCustomer: $error") logger.e("mapErrorForCustomer: $error")
error.mapErrorForCustomer() error.mapErrorForCustomer()
} }
.map { customerInfo -> .map { customerInfo ->
TangemLogger.withTag(TAG).i("customerInfo") logger.i("customerInfo")
if (customerInfo.productInstance == null) { if (customerInfo.productInstance == null) {
onboardingRepository.createOrder(userWalletId) onboardingRepository.createOrder(userWalletId)
MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.NEW) MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.NEW)

View file

@ -184,7 +184,7 @@ internal class PortfolioSelectorModel @Inject constructor(
val account = accountStatus.account val account = accountStatus.account
val accountBalance = when (accountStatus) { val accountBalance = when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance
is AccountStatus.Payment -> accountStatus.totalFiatBalance is AccountStatus.Payment -> accountStatus.value.totalFiatBalance
} }
val accountItemUM = AccountPortfolioItemUMConverter( val accountItemUM = AccountPortfolioItemUMConverter(
onClick = { selectorController.selectAccount(account.accountId) }, onClick = { selectorController.selectAccount(account.accountId) },

View file

@ -19,6 +19,7 @@ import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.TotalFiatBalance
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.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.UserCountry
@ -105,7 +106,7 @@ internal class OnrampTokenListModel @Inject constructor(
updateTokenListUM( updateTokenListUM(
SetLoadingAccountTokenListTransformer( SetLoadingAccountTokenListTransformer(
appCurrency = appCurrency, appCurrency = appCurrency,
accountList = accountList.accountStatuses.toList(), accountList = accountList.accountStatuses.filterCryptoPortfolio().toList(),
isAccountsMode = isAccountsMode, isAccountsMode = isAccountsMode,
), ),
) )
@ -237,6 +238,7 @@ internal class OnrampTokenListModel @Inject constructor(
private fun AccountStatusList.filterAccountsByQuery( private fun AccountStatusList.filterAccountsByQuery(
query: String, query: String,
): Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>> = accountStatuses.asSequence() ): Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>> = accountStatuses.asSequence()
.filterCryptoPortfolio()
.associate { accountStatus -> .associate { accountStatus ->
when (accountStatus) { when (accountStatus) {
is AccountStatus.CryptoPortfolio -> { is AccountStatus.CryptoPortfolio -> {

View file

@ -31,6 +31,7 @@ import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.express.models.ExpressOperationType
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.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.Network
@ -131,7 +132,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
private suspend fun getAccountCurrencyTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { private suspend fun getAccountCurrencyTokensDataState(currency: CryptoCurrency): TokensDataStateExpress {
val walletAccountCurrencyStatuses = singleAccountStatusListSupplier.getSyncOrNull( val walletAccountCurrencyStatuses = singleAccountStatusListSupplier.getSyncOrNull(
SingleAccountStatusListProducer.Params(userWalletId), SingleAccountStatusListProducer.Params(userWalletId),
)?.accountStatuses.orEmpty() )?.accountStatuses.orEmpty().filterCryptoPortfolio()
val walletAccountCurrencyStatusesExceptInitial: Map<Account, List<CryptoCurrencyStatus>> = val walletAccountCurrencyStatusesExceptInitial: Map<Account, List<CryptoCurrencyStatus>> =
walletAccountCurrencyStatuses.mapNotNull { accountStatus -> walletAccountCurrencyStatuses.mapNotNull { accountStatus ->

View file

@ -15,4 +15,6 @@ dependencies {
/** Compose */ /** Compose */
implementation(deps.compose.runtime) implementation(deps.compose.runtime)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
} }

View file

@ -0,0 +1,19 @@
package com.tangem.features.tangempay.component
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.features.tangempay.entity.TangemPayMainUM
@Stable
interface TangemPayMainBlockComponent {
fun LazyListScope.tangemPayMainContent(
state: TangemPayMainUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
)
interface Factory : ComponentFactory<Unit, TangemPayMainBlockComponent>
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.tangempay.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
sealed class TangemPayMainUM {
data object Empty : TangemPayMainUM()
data object Loading : TangemPayMainUM()
data class UnderReview(val subtitle: TextReference, val onClick: () -> Unit) : TangemPayMainUM()
data class IssuingCard(val onClick: () -> Unit) : TangemPayMainUM()
data class FailedToIssue(val onClick: () -> Unit) : TangemPayMainUM()
data class Content(
val subtitle: TextReference,
val isBalanceFlickering: Boolean,
val balance: TextReference,
val balanceSubtitle: TextReference,
val onClick: () -> Unit,
val shouldShowOnlyCacheWarning: Boolean,
) : TangemPayMainUM()
data object TemporaryUnavailable : TangemPayMainUM()
data object SyncNeeded : TangemPayMainUM()
data object ExposedDevice : TangemPayMainUM()
}

View file

@ -15,10 +15,9 @@ dependencies {
/** Core */ /** Core */
implementation(projects.core.decompose) implementation(projects.core.decompose)
implementation(projects.core.ui) implementation(projects.core.ui)
implementation(projects.core.configToggles)
/** Features api */ /** Features api */
implementation(projects.features.tangempay.details.api) implementation(projects.features.tangempay.main.api)
/** Compose */ /** Compose */
implementation(deps.compose.foundation) implementation(deps.compose.foundation)

View file

@ -0,0 +1,37 @@
package com.tangem.features.tangempay.component
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.tangempay.ui.TangemPayMainBlockItem
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
private const val TANGEM_PAY_ACCOUNT_CONTENT_TYPE = "TangemPayAccount"
@Suppress("UnusedPrivateProperty")
internal class DefaultTangemPayMainBlockComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: Unit,
) : TangemPayMainBlockComponent, AppComponentContext by context {
override fun LazyListScope.tangemPayMainContent(
state: TangemPayMainUM,
isBalanceHidden: Boolean,
modifier: Modifier,
) {
item(
key = TANGEM_PAY_ACCOUNT_CONTENT_TYPE,
contentType = TANGEM_PAY_ACCOUNT_CONTENT_TYPE,
) {
TangemPayMainBlockItem(state, isBalanceHidden, modifier)
}
}
@AssistedFactory
interface Factory : TangemPayMainBlockComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultTangemPayMainBlockComponent
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.tangempay.di
import com.tangem.features.tangempay.component.DefaultTangemPayMainBlockComponent
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal interface TangemPayMainModule {
@Binds
fun bindTangemPayMainBlockComponent(
factory: DefaultTangemPayMainBlockComponent.Factory,
): TangemPayMainBlockComponent.Factory
}

View file

@ -0,0 +1,355 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
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.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.inputrow.InputRowImageBase
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.tangempay.main.impl.R
import com.tangem.utils.StringsSigns.DASH_SIGN
private const val DISABLED_ALPHA = 0.6F
@Composable
internal fun TangemPayMainBlockItem(state: TangemPayMainUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
when (state) {
is TangemPayMainUM.Empty -> Unit
is TangemPayMainUM.Loading -> TangemPayMainLoadingItem(modifier)
is TangemPayMainUM.UnderReview -> TangemPayMainUnderReviewItem(state, modifier)
is TangemPayMainUM.IssuingCard -> TangemPayMainIssuingCardItem(state, modifier)
is TangemPayMainUM.FailedToIssue -> TangemPayMainFailedIssueItem(state, modifier)
is TangemPayMainUM.Content -> TangemPayMainBlockContent(state, isBalanceHidden, modifier)
is TangemPayMainUM.TemporaryUnavailable -> TangemPayMainTempUnavailableItem(modifier)
is TangemPayMainUM.SyncNeeded -> TangemPayMainSyncNeededItem(modifier)
is TangemPayMainUM.ExposedDevice -> TangemPayMainExposedDeviceItem(modifier)
}
}
@Composable
private fun TangemPayMainBlockContent(
state: TangemPayMainUM.Content,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier,
shape = TangemTheme.shapes.roundedCornersXMedium,
color = TangemTheme.colors.background.primary,
onClick = state.onClick,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.padding(horizontal = 12.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Image(
painter = painterResource(R.drawable.img_visa_36),
contentDescription = null,
modifier = Modifier.size(36.dp),
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(
text = stringResourceSafe(R.string.tangempay_payment_account),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = state.subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.spacedBy(2.dp),
horizontalAlignment = Alignment.End,
) {
TangemPayFiatAmount(
text = state.balance.resolveReference(),
isBalanceFlickering = state.isBalanceFlickering,
isBalanceFromCache = state.shouldShowOnlyCacheWarning,
isBalanceHidden = isBalanceHidden,
)
Text(
text = state.balanceSubtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.End,
)
}
}
}
}
@Composable
private fun TangemPayFiatAmount(
text: String,
isBalanceFlickering: Boolean,
isBalanceFromCache: Boolean,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(isBalanceFromCache) {
Row(
modifier = Modifier.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
modifier = Modifier.size(12.dp),
painter = painterResource(R.drawable.ic_error_sync_24),
tint = TangemTheme.colors.icon.inactive,
contentDescription = null,
)
}
}
Text(
text = text.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.body2.applyBladeBrush(
isEnabled = isBalanceFlickering,
textColor = TangemTheme.colors.text.primary1,
),
textAlign = TextAlign.End,
)
}
}
@Composable
private fun TangemPayMainUnderReviewItem(state: TangemPayMainUM.UnderReview, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius14))
.background(TangemTheme.colors.background.primary),
onClick = state.onClick,
) {
InputRowImageBase(
modifier = Modifier
.padding(
all = TangemTheme.dimens.spacing12,
),
subtitle = resourceReference(R.string.tangempay_payment_account),
caption = state.subtitle,
subtitleColor = TangemTheme.colors.text.primary1,
captionColor = TangemTheme.colors.text.tertiary,
iconResWebp = R.drawable.img_visa_36,
)
}
}
@Composable
private fun TangemPayMainTempUnavailableItem(modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius14))
.background(TangemTheme.colors.background.primary),
enabled = false,
) {
InputRowImageBase(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
subtitle = resourceReference(R.string.tangempay_payment_account),
caption = TextReference.Str(DASH_SIGN),
subtitleColor = TangemTheme.colors.text.tertiary,
captionColor = TangemTheme.colors.text.tertiary,
iconResWebp = R.drawable.img_visa_36,
)
}
}
@Composable
private fun TangemPayMainIssuingCardItem(state: TangemPayMainUM.IssuingCard, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius14))
.background(TangemTheme.colors.background.primary),
onClick = state.onClick,
) {
InputRowImageBase(
modifier = Modifier
.padding(all = TangemTheme.dimens.spacing12),
subtitle = resourceReference(R.string.tangempay_payment_account),
caption = resourceReference(R.string.tangempay_issuing_your_card),
subtitleColor = TangemTheme.colors.text.primary1,
captionColor = TangemTheme.colors.text.tertiary,
iconResWebp = R.drawable.img_visa_36,
)
}
}
@Composable
private fun TangemPayMainFailedIssueItem(state: TangemPayMainUM.FailedToIssue, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius14))
.background(TangemTheme.colors.background.primary),
onClick = state.onClick,
) {
InputRowImageBase(
modifier = Modifier
.padding(
all = TangemTheme.dimens.spacing12,
),
subtitle = TextReference.Res(R.string.tangempay_payment_account),
caption = TextReference.Res(R.string.tangempay_failed_to_issue_card),
subtitleColor = TangemTheme.colors.text.primary1,
captionColor = TangemTheme.colors.text.tertiary,
iconResWebp = com.tangem.core.ui.R.drawable.img_visa_36,
iconEndRes = R.drawable.ic_alert_24,
endIconTint = TangemTheme.colors.icon.warning,
)
}
}
@Composable
private fun TangemPayMainSyncNeededItem(modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius14))
.background(TangemTheme.colors.background.primary),
enabled = false,
) {
InputRowImageBase(
modifier = Modifier.padding(
all = TangemTheme.dimens.spacing12,
),
subtitle = resourceReference(R.string.tangempay_payment_account),
caption = resourceReference(R.string.tangempay_payment_account_sync_needed),
subtitleColor = TangemTheme.colors.text.tertiary,
captionColor = TangemTheme.colors.text.tertiary,
iconResWebp = R.drawable.img_visa_36,
)
}
}
@Composable
private fun TangemPayMainExposedDeviceItem(modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius14))
.background(TangemTheme.colors.background.primary)
.alpha(DISABLED_ALPHA),
enabled = false,
) {
InputRowImageBase(
modifier = Modifier
.padding(
all = TangemTheme.dimens.spacing12,
),
subtitle = resourceReference(R.string.tangempay_payment_account),
caption = resourceReference(R.string.tangem_pay_rooted_device_subtitle),
subtitleColor = TangemTheme.colors.text.primary1,
captionColor = TangemTheme.colors.text.tertiary,
iconResWebp = R.drawable.img_visa_36,
endIconTint = TangemTheme.colors.icon.warning,
)
}
}
@Composable
private fun TangemPayMainLoadingItem(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.primary, shape = TangemTheme.shapes.roundedCornersXMedium)
.padding(horizontal = 12.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CircleShimmer(modifier = Modifier.size(36.dp))
Column(
modifier = Modifier.padding(start = 12.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
RectangleShimmer(
modifier = Modifier
.padding(vertical = 4.dp)
.sizeIn(minWidth = 70.dp, minHeight = 12.dp),
)
RectangleShimmer(
modifier = Modifier
.padding(vertical = 2.dp)
.sizeIn(minWidth = 52.dp, minHeight = 12.dp),
)
}
SpacerWMax()
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
RectangleShimmer(
modifier = Modifier
.padding(vertical = 4.dp)
.sizeIn(minWidth = 40.dp, minHeight = 12.dp),
)
RectangleShimmer(
modifier = Modifier
.padding(vertical = 2.dp)
.sizeIn(minWidth = 40.dp, minHeight = 12.dp),
)
}
}
}
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview
@Composable
private fun TangemPayMainItemsPreview(
@PreviewParameter(TangemPayMainUMPreviewParameterProvider::class)
state: TangemPayMainUM,
) {
TangemThemePreview {
TangemPayMainBlockItem(state = state, isBalanceHidden = false)
}
}
private class TangemPayMainUMPreviewParameterProvider : CollectionPreviewParameterProvider<TangemPayMainUM>(
collection = listOf(
TangemPayMainUM.Loading,
TangemPayMainUM.SyncNeeded,
TangemPayMainUM.TemporaryUnavailable,
TangemPayMainUM.ExposedDevice,
TangemPayMainUM.FailedToIssue(onClick = {}),
TangemPayMainUM.UnderReview(subtitle = resourceReference(R.string.tangempay_kyc_in_progress), onClick = {}),
TangemPayMainUM.IssuingCard(onClick = {}),
TangemPayMainUM.Content(
subtitle = TextReference.Str("*1234"),
isBalanceFlickering = true,
balance = TextReference.Str("$ 101.56"),
balanceSubtitle = TextReference.Str("USDC"),
onClick = {},
shouldShowOnlyCacheWarning = true,
),
),
)

View file

@ -148,6 +148,7 @@ dependencies {
implementation(projects.features.tangempay.details.api) implementation(projects.features.tangempay.details.api)
implementation(projects.features.feed.api) implementation(projects.features.feed.api)
implementation(projects.features.promoBanners.api) implementation(projects.features.promoBanners.api)
implementation(projects.features.tangempay.main.api)
/** Common modules */ /** Common modules */
implementation(projects.common) implementation(projects.common)

View file

@ -37,6 +37,7 @@ import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.feed.entry.components.FeedEntryComponent import com.tangem.features.feed.entry.components.FeedEntryComponent
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.api.PushNotificationsParams import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import com.tangem.features.send.v2.api.NetworkSelectionComponent import com.tangem.features.send.v2.api.NetworkSelectionComponent
import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent
@ -51,6 +52,7 @@ internal class WalletComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext, @Assisted appComponentContext: AppComponentContext,
@Assisted navigate: (WalletRoute) -> Unit, @Assisted navigate: (WalletRoute) -> Unit,
feedEntryComponentFactory: FeedEntryComponent.Factory, feedEntryComponentFactory: FeedEntryComponent.Factory,
tangemPayMainBlockComponentFactory: TangemPayMainBlockComponent.Factory,
private val renameWalletComponentFactory: RenameWalletComponent.Factory, private val renameWalletComponentFactory: RenameWalletComponent.Factory,
private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory,
private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory,
@ -70,6 +72,12 @@ internal class WalletComponent @AssistedInject constructor(
entryRoute = null, entryRoute = null,
) )
} }
private val tangemPayMainBlockComponent by lazy {
tangemPayMainBlockComponentFactory.create(
context = child("tangemPayMainBlockComponent"),
params = Unit,
)
}
private val promoBannersBlockComponent: PromoBannersBlockComponent? by lazy { private val promoBannersBlockComponent: PromoBannersBlockComponent? by lazy {
if (!newPromoBannersFeatureToggles.isNewPromoBannersEnabled) return@lazy null if (!newPromoBannersFeatureToggles.isNewPromoBannersEnabled) return@lazy null
@ -218,10 +226,11 @@ internal class WalletComponent @AssistedInject constructor(
val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) }
var headerSize by remember { mutableStateOf(0.dp) } var headerSize by remember { mutableStateOf(0.dp) }
val dialog by dialog.subscribeAsState() val dialog by dialog.subscribeAsState()
val uiState by model.uiState.collectAsStateWithLifecycle()
if (designFeatureToggles.isRedesignEnabled) { if (designFeatureToggles.isRedesignEnabled) {
WalletScreen2( WalletScreen2(
state = model.uiState.collectAsStateWithLifecycle().value, state = uiState,
bottomSheetContent = { bottomSheetContent = {
BottomSheetContent( BottomSheetContent(
bottomSheetState = bottomSheetState, bottomSheetState = bottomSheetState,
@ -234,8 +243,9 @@ internal class WalletComponent @AssistedInject constructor(
) )
} else { } else {
WalletScreen( WalletScreen(
state = model.uiState.collectAsStateWithLifecycle().value, state = uiState,
promoBannersBlockComponent = promoBannersBlockComponent, promoBannersBlockComponent = promoBannersBlockComponent,
tangemPayComponent = tangemPayMainBlockComponent,
bottomSheetContent = { bottomSheetContent = {
BottomSheetContent( BottomSheetContent(
bottomSheetState = bottomSheetState, bottomSheetState = bottomSheetState,

View file

@ -6,13 +6,13 @@ import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
@ -25,20 +25,20 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.* import com.tangem.domain.models.wallet.*
import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.qrscanning.models.QrResultSource import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.models.ClassifiedQrContent
import com.tangem.domain.qrscanning.models.QrSendTarget
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.qrscanning.models.ClassifiedQrContent
import com.tangem.domain.qrscanning.models.QrResultSource
import com.tangem.domain.qrscanning.models.QrSendTarget
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ResolveQrSendTargetsUseCase import com.tangem.domain.qrscanning.usecases.ResolveQrSendTargetsUseCase
import com.tangem.domain.settings.* import com.tangem.domain.settings.*
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.wallets.usecase.* import com.tangem.domain.wallets.usecase.*
import com.tangem.domain.wallets.usecase.GetWalletIconUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
@ -61,6 +61,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejec
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.utils.Provider import com.tangem.utils.Provider
@ -119,6 +120,8 @@ internal class WalletModel @Inject constructor(
private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val wcPairService: WcPairService, private val wcPairService: WcPairService,
private val resolveQrSendTargetsUseCase: ResolveQrSendTargetsUseCase, private val resolveQrSendTargetsUseCase: ResolveQrSendTargetsUseCase,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val uiMessageSender: UiMessageSender, private val uiMessageSender: UiMessageSender,
val screenLifecycleProvider: ScreenLifecycleProvider, val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter, val innerWalletRouter: InnerWalletRouter,
@ -427,14 +430,17 @@ internal class WalletModel @Inject constructor(
updateTangemPayJobHolder.cancel() updateTangemPayJobHolder.cancel()
modelScope.launch { modelScope.launch {
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
while (isActive) { while (isActive) {
delay(TANGEM_PAY_UPDATE_INTERVAL) delay(TANGEM_PAY_UPDATE_INTERVAL)
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
} }
}.saveIn(updateTangemPayJobHolder) }.saveIn(updateTangemPayJobHolder)
} else { } else {
// Don't refresh customer info periodically if the card was already issued, only update on swipe to refresh // Don't refresh customer info periodically if the card was already issued, only update on swipe to refresh
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
} }
}.launchIn(modelScope) }.launchIn(modelScope)
} }
@ -543,6 +549,7 @@ internal class WalletModel @Inject constructor(
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
isMainScreenQrScanningEnabled = walletFeatureToggles.isMainScreenQrScanningEnabled, isMainScreenQrScanningEnabled = walletFeatureToggles.isMainScreenQrScanningEnabled,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled,
), ),
) )
@ -589,6 +596,7 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled,
), ),
) )
} }
@ -610,6 +618,7 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled,
), ),
) )
} }
@ -624,6 +633,7 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled,
), ),
) )
@ -685,6 +695,7 @@ internal class WalletModel @Inject constructor(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled,
), ),
) )

View file

@ -23,6 +23,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.model.TangemPayEntryPoint import com.tangem.domain.pay.model.TangemPayEntryPoint
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
@ -30,7 +31,6 @@ import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshShowProgressTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshShowProgressTransformer
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -77,6 +77,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
private val tangemPayEligibilityManager: TangemPayEligibilityManager, private val tangemPayEligibilityManager: TangemPayEligibilityManager,
private val uiMessageSender: UiMessageSender, private val uiMessageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
) : BaseWalletClickIntents(), TangemPayIntents { ) : BaseWalletClickIntents(), TangemPayIntents {
override suspend fun onPullToRefresh() { override suspend fun onPullToRefresh() {
@ -85,20 +86,28 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
return return
} }
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
} }
override fun onRefreshPayToken(userWallet: UserWallet) { override fun onRefreshPayToken(userWallet: UserWallet) {
stateHolder.update(TangemPayRefreshShowProgressTransformer(userWallet.walletId)) stateHolder.update(
TangemPayRefreshShowProgressTransformer(
userWalletId = userWallet.walletId,
shouldShowProgress = true,
),
)
modelScope.launch { modelScope.launch {
produceInitialDataTangemPay.invoke(userWallet.walletId) produceInitialDataTangemPay.invoke(userWallet.walletId)
.onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWallet.walletId) } .onRight {
tangemPayMainScreenCustomerInfoUseCase.fetch(userWallet.walletId)
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWallet.walletId))
}
.onLeft { .onLeft {
stateHolder.update( stateHolder.update(
transformer = TangemPayRefreshNeededStateTransformer( TangemPayRefreshShowProgressTransformer(
userWallet = userWallet,
userWalletId = userWallet.walletId, userWalletId = userWallet.walletId,
onRefreshClick = { onRefreshPayToken(userWallet) }, shouldShowProgress = false,
), ),
) )
} }
@ -267,7 +276,10 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
analyticsEventHandler.send(TangemPayAnalyticsEvents.KycCancelled()) analyticsEventHandler.send(TangemPayAnalyticsEvents.KycCancelled())
modelScope.launch { modelScope.launch {
tangemPayOnboardingRepository.disableTangemPay(userWalletId) tangemPayOnboardingRepository.disableTangemPay(userWalletId)
.onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) } .onRight {
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
}
.onLeft { uiMessageSender.send(ToastMessage(resourceReference(R.string.common_something_went_wrong))) } .onLeft { uiMessageSender.send(ToastMessage(resourceReference(R.string.common_something_went_wrong))) }
} }
} }

View file

@ -18,6 +18,7 @@ import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy.topBarConfig import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy.topBarConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.DASH_SIGN
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
@ -217,6 +218,8 @@ internal object WalletScreenPreviewDataLegacy {
onClick = {}, onClick = {},
), ),
type = WalletType.Cold, type = WalletType.Cold,
tangemPayMainUM = TangemPayMainUM.Empty,
isTangemPayRefactorEnabled = false,
) )
} }

View file

@ -123,6 +123,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.Warning.TangemPayUnreachable -> null is WalletNotification.Warning.TangemPayUnreachable -> null
is WalletNotification.UpgradeHotWalletPromo -> null is WalletNotification.UpgradeHotWalletPromo -> null
is WalletNotification.TokenSyncCompleted -> null is WalletNotification.TokenSyncCompleted -> null
is WalletNotification.CreateTangemPayAccount -> null
} }
} }

View file

@ -7,10 +7,10 @@ import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase
import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase
@ -18,6 +18,8 @@ import com.tangem.domain.hotwallet.GetUpgradeBannerClosureTimestampUseCase
import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
@ -25,7 +27,6 @@ import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
import com.tangem.domain.promo.models.PromoId import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import javax.inject.Inject import javax.inject.Inject
@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]")
@ -69,13 +69,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
@Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType") @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType")
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver
val accountStatusListFlow by lazy {
val params = SingleAccountStatusListProducer.Params(userWallet.walletId) val params = SingleAccountStatusListProducer.Params(userWallet.walletId)
accountDependencies.singleAccountStatusListSupplier(params) val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params)
.map { it.totalFiatBalance to it.flattenCurrencies() }
.map { Lce.Content(it) }
}
return combine( return combine(
accountStatusListFlow, accountStatusListFlow,
@ -95,9 +90,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
.distinctUntilChanged(), .distinctUntilChanged(),
) { array -> array } ) { array -> array }
.map { array -> .map { array ->
val lceTokens = array[0] as Lce<TokenListError, Pair<TotalFiatBalance, List<CryptoCurrencyStatus>>> val accountStatusList = array[0] as AccountStatusList
val totalFiatBalance = lceTokens.map { it.first }
val flattenCurrencies = lceTokens.map { it.second }
val isReadyToShowRating = array[1] as Boolean val isReadyToShowRating = array[1] as Boolean
val isNeedToBackup = array[2] as Boolean val isNeedToBackup = array[2] as Boolean
val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus
@ -108,8 +101,13 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val shouldShowUpgradeBanner = array[8] as Boolean val shouldShowUpgradeBanner = array[8] as Boolean
val closureTimestamp = array[9] as? Long val closureTimestamp = array[9] as? Long
val flattenCurrencies = accountStatusList.flattenCurrencies()
val paymentAccountStatus = accountStatusList.accountStatuses
.filterIsInstance<AccountStatus.Payment>()
.firstOrNull()
buildList { buildList {
addUsedOutdatedDataNotification(totalFiatBalance) addUsedOutdatedDataNotification(accountStatusList.totalFiatBalance)
addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents)
@ -162,24 +160,54 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
if (!hasCriticalOrWarning) { if (!hasCriticalOrWarning) {
addRateTheAppNotification(isReadyToShowRating, clickIntents) addRateTheAppNotification(isReadyToShowRating, clickIntents)
} }
// add as last warning
paymentAccountStatus?.let { paymentAccountStatus ->
addTangemPayWarnings(
status = paymentAccountStatus,
userWallet = userWallet,
walletClickIntents = clickIntents,
)
}
}.toImmutableList() }.toImmutableList()
} }
} }
private fun MutableList<WalletNotification>.addUsedOutdatedDataNotification( private fun MutableList<WalletNotification>.addTangemPayWarnings(
totalFiatBalance: Lce<TokenListError, TotalFiatBalance>, status: AccountStatus.Payment,
userWallet: UserWallet,
walletClickIntents: WalletClickIntents,
) { ) {
val notification = when (status.value) {
is PaymentAccountStatusValue.Error.NotSynced -> WalletNotification.Warning.TangemPayRefreshNeeded(
buttonText = when (userWallet) {
is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan)
is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_restore_access)
},
onRefreshClick = { walletClickIntents.onRefreshPayToken(userWallet) },
shouldShowProgress = false,
)
is PaymentAccountStatusValue.NotCreated -> WalletNotification.CreateTangemPayAccount(
onClick = { walletClickIntents.onOnboardingBannerClick(userWallet.walletId) },
onCloseClick = { walletClickIntents.onOnboardingBannerCloseClick(userWallet.walletId) },
)
is PaymentAccountStatusValue.Error.Unavailable -> WalletNotification.Warning.TangemPayUnreachable
is PaymentAccountStatusValue.Error.CardIssueFailed,
is PaymentAccountStatusValue.Error.ExposedDevice,
is PaymentAccountStatusValue.IssuingCard,
is PaymentAccountStatusValue.Loaded,
is PaymentAccountStatusValue.Loading,
is PaymentAccountStatusValue.Locked,
is PaymentAccountStatusValue.UnderReview,
-> null
}
notification?.let(::add)
}
private fun MutableList<WalletNotification>.addUsedOutdatedDataNotification(totalFiatBalance: TotalFiatBalance) {
addIf( addIf(
element = WalletNotification.UsedOutdatedData, element = WalletNotification.UsedOutdatedData,
condition = totalFiatBalance.fold( condition = (totalFiatBalance as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE,
ifLoading = {
(it as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE
},
ifContent = {
(it as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE
},
ifError = { false },
),
) )
} }
@ -254,7 +282,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private fun MutableList<WalletNotification>.addInformationalNotifications( private fun MutableList<WalletNotification>.addInformationalNotifications(
userWallet: UserWallet, userWallet: UserWallet,
cardTypesResolver: CardTypesResolver?, cardTypesResolver: CardTypesResolver?,
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>, flattenCurrencies: List<CryptoCurrencyStatus>,
clickIntents: WalletClickIntents, clickIntents: WalletClickIntents,
) { ) {
addIf( addIf(
@ -267,7 +295,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private fun MutableList<WalletNotification>.addMissingAddressesNotification( private fun MutableList<WalletNotification>.addMissingAddressesNotification(
userWallet: UserWallet, userWallet: UserWallet,
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>, flattenCurrencies: List<CryptoCurrencyStatus>,
clickIntents: WalletClickIntents, clickIntents: WalletClickIntents,
) { ) {
val currencies = flattenCurrencies.getMissingAddressCurrencies() val currencies = flattenCurrencies.getMissingAddressCurrencies()
@ -285,10 +313,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
) )
} }
private fun Lce<TokenListError, List<CryptoCurrencyStatus>>.getMissingAddressCurrencies(): List<CryptoCurrency> { private fun List<CryptoCurrencyStatus>.getMissingAddressCurrencies(): List<CryptoCurrency> {
val flattenCurrencies = getOrNull(isPartialContentAccepted = true) ?: return emptyList() return this
return flattenCurrencies
.filter { it.value is CryptoCurrencyStatus.MissedDerivation } .filter { it.value is CryptoCurrencyStatus.MissedDerivation }
.map(CryptoCurrencyStatus::currency) .map(CryptoCurrencyStatus::currency)
} }
@ -330,7 +356,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private fun MutableList<WalletNotification>.addWarningNotifications( private fun MutableList<WalletNotification>.addWarningNotifications(
cardTypesResolver: CardTypesResolver?, cardTypesResolver: CardTypesResolver?,
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>, flattenCurrencies: List<CryptoCurrencyStatus>,
isNeedToBackup: Boolean, isNeedToBackup: Boolean,
clickIntents: WalletClickIntents, clickIntents: WalletClickIntents,
) { ) {
@ -355,7 +381,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
} }
private fun MutableList<WalletNotification>.addCloreMigrationNotification( private fun MutableList<WalletNotification>.addCloreMigrationNotification(
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>, flattenCurrencies: List<CryptoCurrencyStatus>,
clickIntents: WalletClickIntents, clickIntents: WalletClickIntents,
) { ) {
val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return
@ -367,10 +393,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
) )
} }
private fun Lce<TokenListError, List<CryptoCurrencyStatus>>.findCloreCurrency(): CryptoCurrencyStatus? { private fun List<CryptoCurrencyStatus>.findCloreCurrency(): CryptoCurrencyStatus? {
val currencies = getOrNull(isPartialContentAccepted = true) ?: return null return this.find { currencyStatus ->
return currencies.find { currencyStatus ->
BlockchainUtils.isClore(currencyStatus.currency.network.rawId) BlockchainUtils.isClore(currencyStatus.currency.network.rawId)
} }
} }
@ -388,10 +412,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
) )
} }
private fun Lce<TokenListError, List<CryptoCurrencyStatus>>.hasUnreachableNetworks(): Boolean { private fun List<CryptoCurrencyStatus>.hasUnreachableNetworks(): Boolean {
val flattenCurrencies = getOrNull(isPartialContentAccepted = false) ?: return false return this.any { it.value is CryptoCurrencyStatus.Unreachable }
return flattenCurrencies.any { it.value is CryptoCurrencyStatus.Unreachable }
} }
// Remove in first iteration of yield supply feature // Remove in first iteration of yield supply feature
@ -427,7 +449,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private fun MutableList<WalletNotification>.addFinishWalletActivationNotification( private fun MutableList<WalletNotification>.addFinishWalletActivationNotification(
userWallet: UserWallet, userWallet: UserWallet,
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>, flattenCurrencies: List<CryptoCurrencyStatus>,
clickIntents: WalletClickIntents, clickIntents: WalletClickIntents,
shouldAccessCodeSkipped: Boolean, shouldAccessCodeSkipped: Boolean,
) { ) {
@ -437,12 +459,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword && val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword &&
!shouldAccessCodeSkipped !shouldAccessCodeSkipped
val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired
val type = flattenCurrencies.getFinishWalletActivationType()
val type = flattenCurrencies.fold(
ifLoading = { return },
ifContent = { it.getFinishWalletActivationType() },
ifError = { WalletActivationBannerType.Attention },
)
addIf( addIf(
element = WalletNotification.FinishWalletActivation( element = WalletNotification.FinishWalletActivation(
@ -465,15 +482,14 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private suspend fun MutableList<WalletNotification>.addUpgradeHotWalletPromoNotification( private suspend fun MutableList<WalletNotification>.addUpgradeHotWalletPromoNotification(
userWallet: UserWallet, userWallet: UserWallet,
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>, flattenCurrencies: List<CryptoCurrencyStatus>,
clickIntents: WalletClickIntents, clickIntents: WalletClickIntents,
shouldShowUpgradeBanner: Boolean, shouldShowUpgradeBanner: Boolean,
closureTimestamp: Long?, closureTimestamp: Long?,
) { ) {
if (userWallet !is UserWallet.Hot) return if (userWallet !is UserWallet.Hot) return
val currencies = flattenCurrencies.getOrNull(isPartialContentAccepted = true).orEmpty() val hasBalance = flattenCurrencies.any { it.value.amount.orZero().isPositive() }
val hasBalance = currencies.any { it.value.amount.orZero().isPositive() }
val shouldShow = checkHotWalletUpgradeBannerUseCase( val shouldShow = checkHotWalletUpgradeBannerUseCase(
walletId = userWallet.walletId, walletId = userWallet.walletId,

View file

@ -151,7 +151,6 @@ sealed class WalletNotification(val config: NotificationConfig) {
) )
data class TangemPayRefreshNeeded( data class TangemPayRefreshNeeded(
@DrawableRes private val tangemIcon: Int?,
private val onRefreshClick: () -> Unit, private val onRefreshClick: () -> Unit,
private val buttonText: TextReference, private val buttonText: TextReference,
private val shouldShowProgress: Boolean, private val shouldShowProgress: Boolean,
@ -160,7 +159,7 @@ sealed class WalletNotification(val config: NotificationConfig) {
subtitle = resourceReference(id = R.string.tangempay_use_tangem_device_to_restore_payment_account), subtitle = resourceReference(id = R.string.tangempay_use_tangem_device_to_restore_payment_account),
buttonsState = ButtonsState.PrimaryButtonConfig( buttonsState = ButtonsState.PrimaryButtonConfig(
text = buttonText, text = buttonText,
iconResId = tangemIcon, iconResId = R.drawable.ic_tangem_24,
onClick = onRefreshClick, onClick = onRefreshClick,
shouldShowProgress = shouldShowProgress, shouldShowProgress = shouldShowProgress,
), ),
@ -480,4 +479,11 @@ sealed class WalletNotification(val config: NotificationConfig) {
), ),
), ),
) )
data class CreateTangemPayAccount(val onClick: () -> Unit, val onCloseClick: () -> Unit) : WalletNotification(
config = NotificationConfig(
subtitle = resourceReference(R.string.tangempay_onboarding_banner_description),
iconResId = R.drawable.img_tangem_pay_visa_banner,
),
)
} }

View file

@ -10,6 +10,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedTx
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedWalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedWalletStateHolder
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.WalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.WalletStateHolder
import com.tangem.features.tangempay.entity.TangemPayMainUM
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentList
@ -24,6 +25,8 @@ internal sealed interface WalletState : WalletStateHolder {
abstract val nftState: WalletNFTItemUM abstract val nftState: WalletNFTItemUM
abstract val type: WalletType abstract val type: WalletType
abstract val tangemPayState: TangemPayState abstract val tangemPayState: TangemPayState
abstract val tangemPayMainUM: TangemPayMainUM
abstract val isTangemPayRefactorEnabled: Boolean // TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED
data class Content( data class Content(
override val pullToRefreshConfig: PullToRefreshConfig, override val pullToRefreshConfig: PullToRefreshConfig,
@ -35,6 +38,8 @@ internal sealed interface WalletState : WalletStateHolder {
override val nftState: WalletNFTItemUM, override val nftState: WalletNFTItemUM,
override val type: WalletType, override val type: WalletType,
override val tangemPayState: TangemPayState, override val tangemPayState: TangemPayState,
override val tangemPayMainUM: TangemPayMainUM,
override val isTangemPayRefactorEnabled: Boolean,
) : MultiCurrency() ) : MultiCurrency()
data class Locked( data class Locked(
@ -54,6 +59,8 @@ internal sealed interface WalletState : WalletStateHolder {
override val tokensListState = WalletTokensListState.ContentState.Locked override val tokensListState = WalletTokensListState.ContentState.Locked
override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden
override val tangemPayState: TangemPayState = TangemPayState.Empty override val tangemPayState: TangemPayState = TangemPayState.Empty
override val tangemPayMainUM: TangemPayMainUM = TangemPayMainUM.Empty
override val isTangemPayRefactorEnabled: Boolean = false
} }
} }

View file

@ -13,6 +13,7 @@ internal class AddWalletTransformer(
private val clickIntents: WalletClickIntents, private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver, private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase, private val getWalletIconUseCase: GetWalletIconUseCase,
private val isTangemPayRefactorEnabled: Boolean,
) : WalletScreenStateTransformer { ) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy { private val walletLoadingStateFactory by lazy {
@ -20,6 +21,7 @@ internal class AddWalletTransformer(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isTangemPayRefactorEnabled = isTangemPayRefactorEnabled,
) )
} }

View file

@ -28,6 +28,7 @@ internal class InitializeWalletsTransformer(
private val walletImageResolver: WalletImageResolver, private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase, private val getWalletIconUseCase: GetWalletIconUseCase,
private val isMainScreenQrScanningEnabled: Boolean = false, private val isMainScreenQrScanningEnabled: Boolean = false,
private val isTangemPayRefactorEnabled: Boolean,
) : WalletScreenStateTransformer { ) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy { private val walletLoadingStateFactory by lazy {
@ -35,6 +36,7 @@ internal class InitializeWalletsTransformer(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isTangemPayRefactorEnabled = isTangemPayRefactorEnabled,
) )
} }

View file

@ -24,6 +24,7 @@ internal class ReinitializeNewWalletTransformer(
private val clickIntents: WalletClickIntents, private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver, private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase, private val getWalletIconUseCase: GetWalletIconUseCase,
private val isTangemPayRefactorEnabled: Boolean,
) : WalletScreenStateTransformer { ) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy { private val walletLoadingStateFactory by lazy {
@ -31,6 +32,7 @@ internal class ReinitializeNewWalletTransformer(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isTangemPayRefactorEnabled = isTangemPayRefactorEnabled,
) )
} }

View file

@ -20,6 +20,7 @@ internal class ReinitializeWalletTransformer(
private val clickIntents: WalletClickIntents, private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver, private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase, private val getWalletIconUseCase: GetWalletIconUseCase,
private val isTangemPayRefactorEnabled: Boolean,
) : WalletStateTransformer(userWalletId = userWallet.walletId) { ) : WalletStateTransformer(userWalletId = userWallet.walletId) {
private val walletLoadingStateFactory by lazy { private val walletLoadingStateFactory by lazy {
@ -27,6 +28,7 @@ internal class ReinitializeWalletTransformer(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isTangemPayRefactorEnabled = isTangemPayRefactorEnabled,
) )
} }

View file

@ -1,6 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingAvailability
@ -8,10 +9,12 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletBalanceUMTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletBalanceUMTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TangemPayMainBlockConverter
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMConverter
import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
import com.tangem.features.tangempay.entity.TangemPayMainUM
import java.math.BigDecimal import java.math.BigDecimal
internal class SetTokenListTransformer( internal class SetTokenListTransformer(
@ -25,12 +28,17 @@ internal class SetTokenListTransformer(
private val isAccountsModeEnabled: Boolean, private val isAccountsModeEnabled: Boolean,
) : WalletStateTransformer(userWallet.walletId) { ) : WalletStateTransformer(userWallet.walletId) {
private val tangemPayConverter by lazy {
TangemPayMainBlockConverter(tangemPayClickIntents = clickIntents)
}
override fun transform(prevState: WalletState): WalletState { override fun transform(prevState: WalletState): WalletState {
return when (prevState) { return when (prevState) {
is WalletState.MultiCurrency.Content -> { is WalletState.MultiCurrency.Content -> {
prevState.copy( prevState.copy(
walletCardState = prevState.walletCardState.toLoadedState(), walletCardState = prevState.walletCardState.toLoadedState(),
tokensListState = prevState.tokensListState.toLoadedState(), tokensListState = prevState.tokensListState.toLoadedState(),
tangemPayMainUM = prevState.tangemPayMainUM.toLoadedState(),
buttons = prevState.enableButtons(), buttons = prevState.enableButtons(),
) )
} }
@ -97,6 +105,17 @@ internal class SetTokenListTransformer(
).convert(value = this) ).convert(value = this)
} }
private fun TangemPayMainUM.toLoadedState(): TangemPayMainUM {
val paymentAccountStatus = when (params) {
is TokenConverterParams.Account -> params.accountList.accountStatuses
.filterIsInstance<AccountStatus.Payment>()
.firstOrNull()
is TokenConverterParams.Wallet -> null
} ?: return this
return tangemPayConverter.convert(paymentAccountStatus)
}
private fun toLoadedState(): WalletTokensListUM { private fun toLoadedState(): WalletTokensListUM {
if (params !is TokenConverterParams.Account) { if (params !is TokenConverterParams.Account) {
return WalletTokensListUM.Empty( return WalletTokensListUM.Empty(

View file

@ -18,7 +18,6 @@ internal class TangemPayRefreshNeededStateTransformer(
override fun transform(prevState: WalletState): WalletState { override fun transform(prevState: WalletState): WalletState {
val tangemPayState = TangemPayState.RefreshNeeded( val tangemPayState = TangemPayState.RefreshNeeded(
notification = TangemPayRefreshNeeded( notification = TangemPayRefreshNeeded(
tangemIcon = R.drawable.ic_tangem_24,
buttonText = when (userWallet) { buttonText = when (userWallet) {
is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan) is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan)
is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_restore_access) is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_restore_access)

View file

@ -5,9 +5,11 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
import kotlinx.collections.immutable.toImmutableList
internal class TangemPayRefreshShowProgressTransformer( internal class TangemPayRefreshShowProgressTransformer(
userWalletId: UserWalletId, userWalletId: UserWalletId,
private val shouldShowProgress: Boolean,
) : WalletStateTransformer(userWalletId) { ) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState { override fun transform(prevState: WalletState): WalletState {
@ -15,11 +17,19 @@ internal class TangemPayRefreshShowProgressTransformer(
val refreshNeededState = multiContentState.tangemPayState as? TangemPayState.RefreshNeeded ?: return prevState val refreshNeededState = multiContentState.tangemPayState as? TangemPayState.RefreshNeeded ?: return prevState
val refreshNotification = val refreshNotification =
refreshNeededState.notification as? WalletNotification.Warning.TangemPayRefreshNeeded ?: return prevState refreshNeededState.notification as? WalletNotification.Warning.TangemPayRefreshNeeded ?: return prevState
val newWarnings = prevState.warnings.map { warning ->
if (warning is WalletNotification.Warning.TangemPayRefreshNeeded) {
warning.copy(shouldShowProgress = shouldShowProgress)
} else {
warning
}
}
return multiContentState.copy( return multiContentState.copy(
tangemPayState = refreshNeededState.copy( tangemPayState = refreshNeededState.copy(
notification = refreshNotification.copy(shouldShowProgress = true), notification = refreshNotification.copy(shouldShowProgress = shouldShowProgress),
), ),
warnings = newWarnings.toImmutableList(),
) )
} }

View file

@ -18,6 +18,7 @@ internal class UnlockWalletTransformer(
private val clickIntents: WalletClickIntents, private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver, private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase, private val getWalletIconUseCase: GetWalletIconUseCase,
private val isTangemPayRefactorEnabled: Boolean,
) : WalletScreenStateTransformer { ) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy { private val walletLoadingStateFactory by lazy {
@ -25,6 +26,7 @@ internal class UnlockWalletTransformer(
clickIntents = clickIntents, clickIntents = clickIntents,
walletImageResolver = walletImageResolver, walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase, getWalletIconUseCase = getWalletIconUseCase,
isTangemPayRefactorEnabled = isTangemPayRefactorEnabled,
) )
} }

View file

@ -0,0 +1,110 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.models.StatusSource
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.pay.TangemPayDetailsConfig
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
import java.util.Currency
private const val POLYGON_CHAIN_ID = 137
internal class TangemPayMainBlockConverter(
private val tangemPayClickIntents: TangemPayIntents,
) : Converter<AccountStatus.Payment, TangemPayMainUM> {
@Suppress("LongMethod", "CyclomaticComplexMethod")
override fun convert(value: AccountStatus.Payment): TangemPayMainUM {
return when (val statusValue = value.value) {
is PaymentAccountStatusValue.Error.CardIssueFailed -> TangemPayMainUM.FailedToIssue(
onClick = { tangemPayClickIntents.onIssuingFailedClicked(statusValue.customerId) },
)
is PaymentAccountStatusValue.Error.ExposedDevice -> TangemPayMainUM.ExposedDevice
is PaymentAccountStatusValue.Error.NotSynced -> TangemPayMainUM.SyncNeeded
is PaymentAccountStatusValue.Error.Unavailable -> TangemPayMainUM.TemporaryUnavailable
is PaymentAccountStatusValue.IssuingCard -> TangemPayMainUM.IssuingCard(
onClick = { tangemPayClickIntents.onIssuingCardClicked() },
)
is PaymentAccountStatusValue.UnderReview -> TangemPayMainUM.UnderReview(
subtitle = when (statusValue.kycStatus) {
KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed)
else -> TextReference.Res(R.string.tangempay_kyc_in_progress)
},
onClick = {
when (statusValue.kycStatus) {
KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked(
userWalletId = value.account.userWalletId,
customerId = statusValue.customerId,
)
else -> tangemPayClickIntents.onKycProgressClicked(value.account.userWalletId)
}
},
)
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,
),
)
},
)
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,
),
)
},
)
}
}
private fun getBalanceText(currencyCode: String, balance: BigDecimal): TextReference {
val currency = Currency.getInstance(currencyCode)
val formattedBalance = balance.format {
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
}
return stringReference(formattedBalance)
}
}

View file

@ -17,6 +17,7 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.utils.extensions.addIf import com.tangem.utils.extensions.addIf
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentList
@ -33,6 +34,7 @@ internal class WalletLoadingStateFactory(
private val clickIntents: WalletClickIntents, private val clickIntents: WalletClickIntents,
private val walletImageResolver: WalletImageResolver, private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase, private val getWalletIconUseCase: GetWalletIconUseCase,
private val isTangemPayRefactorEnabled: Boolean,
) { ) {
fun create(userWallet: UserWallet): WalletState { fun create(userWallet: UserWallet): WalletState {
@ -82,6 +84,8 @@ internal class WalletLoadingStateFactory(
nftState = WalletNFTItemUM.Hidden, nftState = WalletNFTItemUM.Hidden,
type = WalletType.Hot, type = WalletType.Hot,
tangemPayState = TangemPayState.Empty, tangemPayState = TangemPayState.Empty,
tangemPayMainUM = TangemPayMainUM.Empty,
isTangemPayRefactorEnabled = isTangemPayRefactorEnabled,
) )
} }
@ -96,6 +100,8 @@ internal class WalletLoadingStateFactory(
nftState = WalletNFTItemUM.Hidden, nftState = WalletNFTItemUM.Hidden,
type = WalletType.Cold, type = WalletType.Cold,
tangemPayState = TangemPayState.Empty, tangemPayState = TangemPayState.Empty,
tangemPayMainUM = TangemPayMainUM.Empty,
isTangemPayRefactorEnabled = isTangemPayRefactorEnabled,
) )
} }

View file

@ -49,7 +49,6 @@ import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetCo
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheet import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheet
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.expressTransactionsItems import com.tangem.common.ui.expressStatus.expressTransactionsItems
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.atoms.handComposableComponentHeight
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggableHeaderLegacy import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggableHeaderLegacy
@ -60,6 +59,7 @@ import com.tangem.core.ui.components.sheetscaffold.*
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar
import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.softLayerShadow import com.tangem.core.ui.extensions.softLayerShadow
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
@ -84,6 +84,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import com.tangem.features.tangempay.entity.TangemPayMainUM
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -92,6 +94,7 @@ import kotlin.math.roundToInt
@Composable @Composable
internal fun WalletScreen( internal fun WalletScreen(
state: WalletScreenState, state: WalletScreenState,
tangemPayComponent: TangemPayMainBlockComponent,
promoBannersBlockComponent: ComposableContentComponent? = null, promoBannersBlockComponent: ComposableContentComponent? = null,
bottomSheetContent: @Composable (() -> Unit), bottomSheetContent: @Composable (() -> Unit),
bottomSheetHeaderHeightProvider: () -> Dp, bottomSheetHeaderHeightProvider: () -> Dp,
@ -106,6 +109,7 @@ internal fun WalletScreen(
WalletContent( WalletContent(
state = state, state = state,
tangemPayComponent = tangemPayComponent,
walletsListState = walletsListState, walletsListState = walletsListState,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
isAutoScroll = isAutoScroll, isAutoScroll = isAutoScroll,
@ -128,6 +132,7 @@ internal fun WalletScreen(
@Composable @Composable
private fun WalletContent( private fun WalletContent(
state: WalletScreenState, state: WalletScreenState,
tangemPayComponent: TangemPayMainBlockComponent,
walletsListState: LazyListState, walletsListState: LazyListState,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
isAutoScroll: State<Boolean>, isAutoScroll: State<Boolean>,
@ -220,18 +225,12 @@ private fun WalletContent(
} }
} }
if (selectedWallet is WalletState.MultiCurrency) { tangemPayItem(
item(
key = "TangemPayMainScreenBlock",
contentType = selectedWallet.tangemPayState::class.java,
) {
TangemPayMainScreenBlock(
state = selectedWallet.tangemPayState,
isBalanceHidden = state.isHidingMode,
modifier = itemModifier, modifier = itemModifier,
state = selectedWallet,
isHidingMode = state.isHidingMode,
tangemPayComponent = tangemPayComponent,
) )
}
}
(selectedWallet as? WalletState.SingleCurrency)?.let { walletState -> (selectedWallet as? WalletState.SingleCurrency)?.let { walletState ->
walletState.marketPriceBlockState?.let { marketPriceBlockState -> walletState.marketPriceBlockState?.let { marketPriceBlockState ->
@ -749,6 +748,25 @@ internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modi
} }
} }
internal fun LazyListScope.tangemPayItem(
state: WalletState,
isHidingMode: Boolean,
tangemPayComponent: TangemPayMainBlockComponent,
modifier: Modifier = Modifier,
) {
if (state !is WalletState.MultiCurrency) return
if (state.isTangemPayRefactorEnabled) {
with(tangemPayComponent) {
tangemPayMainContent(modifier = modifier, state = state.tangemPayMainUM, isBalanceHidden = isHidingMode)
}
} else {
item(key = "TangemPayMainScreenBlock", contentType = state.tangemPayState::class.java) {
TangemPayMainScreenBlock(modifier = modifier, state = state.tangemPayState, isBalanceHidden = isHidingMode)
}
}
}
@Composable @Composable
private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
if (bottomSheetConfig != null) { if (bottomSheetConfig != null) {
@ -767,6 +785,14 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider::
TangemThemePreview { TangemThemePreview {
WalletScreen( WalletScreen(
state = data, state = data,
tangemPayComponent = object : TangemPayMainBlockComponent {
override fun LazyListScope.tangemPayMainContent(
state: TangemPayMainUM,
isBalanceHidden: Boolean,
modifier: Modifier,
) {
}
},
bottomSheetContent = { bottomSheetContent = {
Text("Markets Content") Text("Markets Content")
}, },

View file

@ -3,10 +3,13 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.tangem.common.ui.notifications.CreatePaymentAccountNotification
import com.tangem.core.ui.components.notifications.NoteMigrationNotification import com.tangem.core.ui.components.notifications.NoteMigrationNotification
import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.ForceDarkTheme import com.tangem.core.ui.res.ForceDarkTheme
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -49,6 +52,16 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
) )
} }
} }
is WalletNotification.CreateTangemPayAccount -> {
CreatePaymentAccountNotification(
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
onClick = item.onClick,
onCloseClick = item.onCloseClick,
image = R.drawable.img_tangem_pay_visa_banner,
title = resourceReference(R.string.tangempay_onboarding_banner_title),
subtitle = resourceReference(R.string.tangempay_onboarding_banner_description),
)
}
else -> { else -> {
Notification( Notification(
config = item.config, config = item.config,

View file

@ -13,6 +13,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock
@ -41,7 +42,6 @@ private fun TangemPayMainScreenBlockPreview() {
TangemPayMainScreenBlock( TangemPayMainScreenBlock(
state = TangemPayState.RefreshNeeded( state = TangemPayState.RefreshNeeded(
TangemPayRefreshNeeded( TangemPayRefreshNeeded(
tangemIcon = R.drawable.ic_tangem_24,
buttonText = resourceReference(id = R.string.home_button_scan), buttonText = resourceReference(id = R.string.home_button_scan),
onRefreshClick = {}, onRefreshClick = {},
shouldShowProgress = false, shouldShowProgress = false,
@ -49,13 +49,30 @@ private fun TangemPayMainScreenBlockPreview() {
), ),
isBalanceHidden = false, isBalanceHidden = false,
) )
TangemPayMainScreenBlock(
state = TangemPayState.TemporaryUnavailable(WalletNotification.Warning.TangemPayUnreachable),
isBalanceHidden = false,
)
TangemPayMainScreenBlock(
state = TangemPayState.OnboardingBanner(onClick = {}, closeOnClick = {}),
isBalanceHidden = false,
)
TangemPayMainScreenBlock(state = TangemPayState.ExposedDevice, isBalanceHidden = false) TangemPayMainScreenBlock(state = TangemPayState.ExposedDevice, isBalanceHidden = false)
TangemPayMainScreenBlock(
state = TangemPayState.FailedIssue(
title = TextReference.Res(R.string.tangempay_payment_account),
description = TextReference.Res(R.string.tangempay_failed_to_issue_card),
iconRes = R.drawable.ic_alert_24,
onButtonClick = { },
),
isBalanceHidden = false,
)
TangemPayMainScreenBlock( TangemPayMainScreenBlock(
Progress( Progress(
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), title = TextReference.Res(R.string.tangempay_payment_account),
description = TextReference.EMPTY, description = TextReference.Res(R.string.tangempay_kyc_in_progress),
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36, iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = {}, onButtonClick = {},
@ -65,19 +82,8 @@ private fun TangemPayMainScreenBlockPreview() {
TangemPayMainScreenBlock( TangemPayMainScreenBlock(
Progress( Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title), title = TextReference.Res(R.string.tangempay_payment_account),
description = TextReference.EMPTY, description = TextReference.Res(R.string.tangempay_issuing_your_card),
buttonText = TextReference.Res(R.string.common_continue),
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = {},
),
isBalanceHidden = false,
)
TangemPayMainScreenBlock(
Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
description = TextReference.Res(R.string.tangempay_issue_card_notification_description),
buttonText = TextReference.EMPTY, buttonText = TextReference.EMPTY,
iconRes = R.drawable.ic_tangem_pay_promo_card_36, iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = {}, onButtonClick = {},

View file

@ -67,7 +67,6 @@ private fun TangemPayRefreshBlockPreview() {
TangemPayRefreshBlock( TangemPayRefreshBlock(
state = TangemPayState.RefreshNeeded( state = TangemPayState.RefreshNeeded(
TangemPayRefreshNeeded( TangemPayRefreshNeeded(
tangemIcon = R.drawable.ic_tangem_24,
buttonText = resourceReference(id = R.string.tangempay_sync_needed_restore_access), buttonText = resourceReference(id = R.string.tangempay_sync_needed_restore_access),
onRefreshClick = {}, onRefreshClick = {},
shouldShowProgress = true, shouldShowProgress = true,