Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-20 21:03:08 +05:00
parent a41dd9aace
commit 41fb5d5bcc
55 changed files with 709 additions and 173 deletions

View file

@ -15,7 +15,7 @@ interface AccountCreateEditComponent : ComposableContentComponent {
) : Params
data class Edit(
val account: Account,
val account: Account.CryptoPortfolio,
) : Params
}
}

View file

@ -7,5 +7,5 @@ import com.tangem.domain.models.account.Account
interface AccountDetailsComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Params, AccountDetailsComponent>
data class Params(val account: Account)
data class Params(val account: Account.CryptoPortfolio)
}

View file

@ -25,7 +25,6 @@ import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase
import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.account.derivationIndex
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents
@ -73,7 +72,7 @@ internal class AccountCreateEditModel @Inject constructor(
when (params) {
is AccountCreateEditComponent.Params.Create -> updateDerivationInfo(userWalletId = params.userWalletId)
is AccountCreateEditComponent.Params.Edit -> {
val derivationIndex = params.account.derivationIndex?.value
val derivationIndex = params.account.derivationIndex.value
val event = AccountSettingsAnalyticEvents.AccountEditScreenOpened(derivationIndex)
analyticsEventHandler.send(event)
}
@ -170,7 +169,7 @@ internal class AccountCreateEditModel @Inject constructor(
val icon = CryptoPortfolioIconConverter.convertBack(state.account.portfolioIcon)
val isNewName = name != params.account.accountName
val isNewIcon = icon != params.account.portfolioIcon
val derivationIndex = params.account.derivationIndex?.value
val derivationIndex = params.account.derivationIndex.value
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon, derivationIndex))
uiState.value = uiState.value.toggleProgress(showProgress = true)
@ -182,7 +181,7 @@ internal class AccountCreateEditModel @Inject constructor(
uiState.value = uiState.value.toggleProgress(showProgress = false)
result
.onLeft { error -> handleEditAccountError(error, params.account.derivationIndex?.value) }
.onLeft { error -> handleEditAccountError(error, params.account.derivationIndex.value) }
.onRight {
showMessage(R.string.account_edit_success_message)
router.pop()

View file

@ -14,12 +14,10 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.derivationIndex
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.account.AccountDetailsComponent
@ -54,29 +52,29 @@ internal class AccountDetailsModel @Inject constructor(
init {
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountSettingsScreenOpened())
singleAccountSupplier(SingleAccountProducer.Params(accountId))
singleAccountSupplier.filterCryptoPortfolioAccount(accountId)
.onEach { account -> uiState.update { buildUI(account) } }
.launchIn(modelScope)
}
private fun onEditAccountClick(account: Account) {
private fun onEditAccountClick(account: Account.CryptoPortfolio) {
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonEdit())
router.push(AppRoute.EditAccount(account))
}
private fun onManageTokensClick(account: Account) {
private fun onManageTokensClick(account: Account.CryptoPortfolio) {
val route = AppRoute.ManageTokens(
source = AppRoute.ManageTokens.Source.ACCOUNT,
portfolioId = PortfolioId(account.accountId),
)
analyticsEventHandler.send(
AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex?.value),
AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex.value),
)
router.push(route)
}
private fun onArchiveAccountClick() {
val accountDerivation = params.account.derivationIndex?.value
val accountDerivation = params.account.derivationIndex.value
val event = AccountSettingsAnalyticEvents.ButtonArchiveAccount(accountDerivation)
analyticsEventHandler.send(event)
confirmArchiveDialog()
@ -86,7 +84,7 @@ internal class AccountDetailsModel @Inject constructor(
val secondAction = EventMessageAction(
title = resourceReference(R.string.common_cancel),
onClick = {
val accountDerivation = params.account.derivationIndex?.value
val accountDerivation = params.account.derivationIndex.value
val event = AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation(accountDerivation)
analyticsEventHandler.send(event)
},
@ -107,7 +105,7 @@ internal class AccountDetailsModel @Inject constructor(
}
private fun archiveCryptoPortfolio() = modelScope.launch {
val accountDerivation = params.account.derivationIndex?.value
val accountDerivation = params.account.derivationIndex.value
val event = AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation(accountDerivation)
analyticsEventHandler.send(event)
uiState.update { it.toggleProgress(true) }
@ -128,7 +126,7 @@ internal class AccountDetailsModel @Inject constructor(
val event = AccountSettingsAnalyticEvents.AccountError(
source = AccountSettingsAnalyticEvents.Source.ARCHIVE,
error = error.tag,
accountDerivation = params.account.derivationIndex?.value,
accountDerivation = params.account.derivationIndex.value,
)
analyticsEventHandler.send(event)
val titleRes: Int
@ -155,16 +153,13 @@ internal class AccountDetailsModel @Inject constructor(
messageSender.send(dialogMessage)
}
private fun buildUI(account: Account): AccountDetailsUM {
val archiveMode = when (account) {
is Account.CryptoPortfolio -> when (account.isMainAccount) {
true -> ArchiveMode.None
false -> ArchiveMode.Available(
onArchiveAccountClick = ::onArchiveAccountClick,
isLoading = false,
)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
private fun buildUI(account: Account.CryptoPortfolio): AccountDetailsUM {
val archiveMode = when (account.isMainAccount) {
true -> ArchiveMode.None
false -> ArchiveMode.Available(
onArchiveAccountClick = ::onArchiveAccountClick,
isLoading = false,
)
}
val isMultiCurrency = getUserWalletUseCase(account.accountId.userWalletId).getOrNull()
?.isMultiCurrency == true

View file

@ -26,7 +26,6 @@ import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.derivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
@ -277,9 +276,9 @@ internal class HotCryptoModel @Inject constructor(
private fun updateCryptoCurrency(
cryptoCurrency: CryptoCurrency,
userWallet: UserWallet,
account: AccountStatus,
account: AccountStatus.CryptoPortfolio,
): CryptoCurrency? {
val derivationIndex = account.account.derivationIndex ?: return null
val derivationIndex = account.account.derivationIndex
val blockchain = cryptoCurrency.network.toBlockchain()
val network = networkFactory.create(

View file

@ -0,0 +1,5 @@
package com.tangem.features.tangempay
interface TangemPayFeatureToggles {
val isTangemPayAccountsRefactorEnabled: Boolean
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.tangempay
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
internal class DefaultTangemPayFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TangemPayFeatureToggles {
override val isTangemPayAccountsRefactorEnabled
get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED")
}

View file

@ -45,7 +45,6 @@ internal class TangemPayDetailsComponent(
appComponentContext = child("txHistoryComponent"),
params = DefaultTangemPayTxHistoryComponent.Params(
userWalletId = params.userWalletId,
customerWalletAddress = params.config.customerWalletAddress,
uiActions = model,
),
)

View file

@ -25,7 +25,6 @@ internal class DefaultTangemPayTxHistoryComponent(
data class Params(
val userWalletId: UserWalletId,
val customerWalletAddress: String,
val uiActions: TangemPayTxHistoryUiActions,
)
}

View file

@ -0,0 +1,21 @@
package com.tangem.features.tangempay.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.tangempay.DefaultTangemPayFeatureToggles
import com.tangem.features.tangempay.TangemPayFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object TangemPayDetailsModule {
@Provides
@Singleton
fun provideTangemPayFeatureToggles(featureTogglesManager: FeatureTogglesManager): TangemPayFeatureToggles {
return DefaultTangemPayFeatureToggles(featureTogglesManager)
}
}

View file

@ -31,7 +31,6 @@ internal class TangemPayTxHistoryModel @Inject constructor(
private val listManager = TangemPayTxHistoryListManager(
repository = tangemPayTxHistoryRepository,
dispatchers = dispatchers,
customerWalletAddress = params.customerWalletAddress,
txHistoryUiActions = params.uiActions,
)
@ -104,7 +103,7 @@ internal class TangemPayTxHistoryModel @Inject constructor(
}
private fun loadMoreItems(): Boolean {
modelScope.launch { listManager.loadMore(params.customerWalletAddress) }
modelScope.launch { listManager.loadMore() }
return true
}

View file

@ -22,7 +22,6 @@ private typealias TangemPayTxHistoryBatchAction = BatchAction<Int, TangemPayTxHi
internal class TangemPayTxHistoryListManager(
private val repository: TangemPayTxHistoryRepository,
private val dispatchers: CoroutineDispatcherProvider,
private val customerWalletAddress: String,
private val txHistoryUiActions: TangemPayTxHistoryUiActions,
) {
private val jobHolder = JobHolder()
@ -56,20 +55,13 @@ internal class TangemPayTxHistoryListManager(
suspend fun reload() {
actionsFlow.emit(
BatchAction.Reload(
requestParams = TangemPayTxHistoryListConfig(
customerWalletAddress = customerWalletAddress,
shouldRefresh = true,
),
),
BatchAction.Reload(requestParams = TangemPayTxHistoryListConfig(shouldRefresh = true)),
)
}
suspend fun loadMore(customerWalletAddress: String) {
suspend fun loadMore() {
actionsFlow.emit(
BatchAction.LoadMore(
requestParams = TangemPayTxHistoryListConfig(customerWalletAddress, shouldRefresh = false),
),
BatchAction.LoadMore(requestParams = TangemPayTxHistoryListConfig(shouldRefresh = false)),
)
}

View file

@ -11,9 +11,9 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.model.CustomerInfo.KycStatus
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents

View file

@ -108,6 +108,7 @@ dependencies {
implementation(projects.features.sendV2.api)
implementation(projects.features.tokenRecieve.api)
implementation(projects.features.yieldSupply.api)
implementation(projects.features.tangempay.details.api)
implementation(deps.decompose.ext.compose)

View file

@ -23,6 +23,7 @@ import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionTrigger
import dagger.assisted.Assisted
@ -48,6 +49,7 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
private val walletBalanceFetcher: WalletBalanceFetcher,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
) : TokenDetailsDeepLinkHandler {
@ -128,7 +130,10 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
id = cryptoCurrency.id,
)
!isMultiCurrency -> walletBalanceFetcher(
params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId),
params = WalletBalanceFetcher.Params(
userWalletId = userWallet.walletId,
isPaymentAccountRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled,
),
)
}
}

View file

@ -145,7 +145,7 @@ internal class AccountItemsDelegate @Inject constructor(
return this.sortedBy { positionByAccountId[it.id] ?: Int.MAX_VALUE }
}
private fun openAccountDetails(account: Account) {
private fun openAccountDetails(account: Account.CryptoPortfolio) {
router.push(AppRoute.AccountDetails(account))
}

View file

@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
@ -29,7 +29,7 @@ internal class WalletTangemPayAnalyticsEventSender @Inject constructor(
// ignore cancelled state on analytics
customerInfo.orderStatus == OrderStatus.CANCELED -> return
// ignore kyc not approved state on analytics
customerInfo.info.kycStatus != CustomerInfo.KycStatus.APPROVED -> return
customerInfo.info.kycStatus != KycStatus.APPROVED -> return
cardInfo != null && productInstance != null -> return
else -> TangemPayAnalyticsEvents.IssuingBannerDisplayed()
}

View file

@ -1,7 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveInAndJoin
@ -27,6 +28,7 @@ import javax.inject.Singleton
internal class WalletContentFetcher @Inject constructor(
private val walletBalanceFetcher: WalletBalanceFetcher,
private val dispatchers: CoroutineDispatcherProvider,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
) {
private val fetchingJobMap = ConcurrentHashMap<UserWalletId, JobHolder>()
@ -64,8 +66,12 @@ internal class WalletContentFetcher @Inject constructor(
Timber.d("Start fetching for $userWalletId")
val maybeResult = launch {
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
.onLeft(Timber::e)
walletBalanceFetcher(
params = WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled,
),
).onLeft(Timber::e)
}
.saveInAndJoin(jobHolder)

View file

@ -5,6 +5,7 @@ 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.kyc.KycStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
@ -16,8 +17,6 @@ import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents
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.WalletState
import com.tangem.domain.pay.model.CustomerInfo.KycStatus.APPROVED
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
import java.util.Currency
@ -55,7 +54,7 @@ internal class TangemPayUpdateInfoStateTransformer(
// when statement copied to WalletTangemPayAnalyticsEventSender. Be careful when editing.
return when {
value.orderStatus == OrderStatus.CANCELED -> createCancelledState(customerId)
value.info.kycStatus != APPROVED && !value.info.customerId.isNullOrEmpty() ->
value.info.kycStatus != KycStatus.APPROVED && !value.info.customerId.isNullOrEmpty() ->
createKycInProgressState(kycStatus = value.info.kycStatus, customerId = customerId)
cardInfo != null && productInstance != null ->
getCardInfoState(customerId, cardInfo, productInstance)
@ -79,7 +78,6 @@ internal class TangemPayUpdateInfoStateTransformer(
cardId = productInstance.cardId,
isPinSet = cardInfo.isPinSet,
cardFrozenState = cardFrozenState,
customerWalletAddress = cardInfo.customerWalletAddress,
cardNumberEnd = cardInfo.lastFourDigits,
chainId = POLYGON_CHAIN_ID,
),
@ -94,25 +92,24 @@ internal class TangemPayUpdateInfoStateTransformer(
}
}
private fun createKycInProgressState(kycStatus: CustomerInfo.KycStatus, customerId: String): TangemPayState =
Progress(
title = TextReference.Res(R.string.tangempay_payment_account),
description = when (kycStatus) {
CustomerInfo.KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed)
else -> TextReference.Res(R.string.tangempay_kyc_in_progress)
},
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = {
when (kycStatus) {
CustomerInfo.KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked(
userWalletId = userWalletId,
customerId = customerId,
)
else -> tangemPayClickIntents.onKycProgressClicked(userWalletId)
}
},
)
private fun createKycInProgressState(kycStatus: KycStatus, customerId: String): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_payment_account),
description = when (kycStatus) {
KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed)
else -> TextReference.Res(R.string.tangempay_kyc_in_progress)
},
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = {
when (kycStatus) {
KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked(
userWalletId = userWalletId,
customerId = customerId,
)
else -> tangemPayClickIntents.onKycProgressClicked(userWalletId)
}
},
)
private fun createIssueProgressState(): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_payment_account),

View file

@ -8,10 +8,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
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.Progress
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock
@Composable
@ -36,6 +38,17 @@ private fun TangemPayMainScreenBlockPreview() {
TangemThemePreview {
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
TangemPayMainScreenBlock(state = TangemPayState.Loading, isBalanceHidden = false)
TangemPayMainScreenBlock(
state = TangemPayState.RefreshNeeded(
TangemPayRefreshNeeded(
tangemIcon = R.drawable.ic_tangem_24,
buttonText = resourceReference(id = R.string.home_button_scan),
onRefreshClick = {},
shouldShowProgress = false,
),
),
isBalanceHidden = false,
)
TangemPayMainScreenBlock(state = TangemPayState.ExposedDevice, isBalanceHidden = false)