Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-24 11:42:34 +03:00
commit c8024b166d
656 changed files with 18270 additions and 4656 deletions

View file

@ -11,12 +11,11 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.nft.FetchNFTCollectionsUseCase
import com.tangem.domain.settings.*
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
@ -44,7 +43,6 @@ import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -79,8 +77,6 @@ internal class WalletModel @Inject constructor(
private val onrampStatusFactory: OnrampStatusFactory,
private val biometryFeatureToggles: BiometryFeatureToggles,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val fetchNFTCollectionsUseCase: FetchNFTCollectionsUseCase,
private val walletsRepository: WalletsRepository,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter,
@ -92,7 +88,6 @@ internal class WalletModel @Inject constructor(
private val walletsUpdateJobHolder = JobHolder()
private val refreshWalletJobHolder = JobHolder()
private val expressStatusJobHolder = JobHolder()
private val walletsNFTsUpdateJobHolder = JobHolder()
private var needToRefreshWallet = false
private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>()
@ -107,10 +102,7 @@ internal class WalletModel @Inject constructor(
subscribeToUserWalletsUpdates()
subscribeOnBalanceHiding()
subscribeOnSelectedWalletFlow()
subscribeToScreenBackgroundState()
subscribeOnPushNotificationsPermission()
subscribeOnExpressTransactionsUpdates()
subscribeOnNFTUpdates()
clickIntents.initialize(innerWalletRouter, modelScope)
}
@ -223,6 +215,8 @@ internal class WalletModel @Inject constructor(
}
walletDeepLinksHandler.registerForWallet(scope = modelScope, userWallet = selectedWallet)
subscribeOnExpressTransactionsUpdates(selectedWallet)
subscribeToScreenBackgroundState(selectedWallet)
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
@ -231,7 +225,7 @@ internal class WalletModel @Inject constructor(
// We need to update the current wallet quotes if the application was in the background for more than 10 seconds
// and then returned to the foreground
private fun subscribeToScreenBackgroundState() {
private fun subscribeToScreenBackgroundState(userWallet: UserWallet) {
screenLifecycleProvider.isBackgroundState
.onEach { isBackground ->
expressTxStatusTaskScheduler.cancelTask()
@ -241,53 +235,31 @@ internal class WalletModel @Inject constructor(
isBackground -> needToRefreshTimer()
needToRefreshWallet && !isBackground -> {
triggerRefreshWalletQuotes()
subscribeOnExpressTransactionsUpdates()
subscribeOnExpressTransactionsUpdates(userWallet)
}
!isBackground -> subscribeOnExpressTransactionsUpdates()
!isBackground -> subscribeOnExpressTransactionsUpdates(userWallet)
}
}
.launchIn(modelScope)
.saveIn(expressStatusJobHolder)
}
private fun subscribeOnExpressTransactionsUpdates() {
modelScope.launch(dispatchers.main) {
expressTxStatusTaskScheduler.cancelTask()
expressTxStatusTaskScheduler.scheduleTask(
modelScope,
PeriodicTask(
isDelayFirst = false,
delay = EXPRESS_STATUS_UPDATE_DELAY,
task = {
runCatching { onrampStatusFactory.updateOnrmapTransactionStatuses() }
},
onSuccess = { /* no-op */ },
onError = { /* no-op */ },
),
)
}.saveIn(expressStatusJobHolder)
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun subscribeOnNFTUpdates() {
getWalletsUseCase()
.conflate()
.distinctUntilChanged()
.flatMapLatest { wallets ->
wallets
.map { wallet ->
walletsRepository.nftEnabledStatus(wallet.walletId)
.distinctUntilChanged()
.onEach { nftEnabled ->
if (nftEnabled) {
fetchNFTCollectionsUseCase.invoke(wallet.walletId)
}
}
private fun subscribeOnExpressTransactionsUpdates(userWallet: UserWallet) {
expressTxStatusTaskScheduler.cancelTask()
expressTxStatusTaskScheduler.scheduleTask(
modelScope,
PeriodicTask(
isDelayFirst = false,
delay = EXPRESS_STATUS_UPDATE_DELAY,
task = {
runCatching {
onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet)
}
.merge()
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
.saveIn(walletsNFTsUpdateJobHolder)
},
onSuccess = { /* no-op */ },
onError = { /* no-op */ },
),
)
}
private fun needToRefreshTimer() {

View file

@ -1,9 +1,16 @@
package com.tangem.feature.wallet.child.wallet.model.intents
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.visa.GetVisaCurrencyUseCase
import com.tangem.domain.visa.GetVisaTxDetailsUseCase
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.BalancesAndLimitsBottomSheetConverter
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.VisaTxDetailsBottomSheetConverter
@ -20,22 +27,30 @@ internal interface VisaWalletIntents {
fun onVisaTransactionClick(id: String)
fun onExploreClick(exploreUrl: String)
fun onDisputeClick(txDetails: VisaTxDetails)
}
@Suppress("LongParameterList")
@ModelScoped
internal class VisaWalletIntentsImplementor @Inject constructor(
private val stateController: WalletStateController,
private val eventSender: WalletEventSender,
private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase,
private val getVisaTxDetailsUseCase: GetVisaTxDetailsUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val getUserWalletsUseCase: GetWalletsUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : BaseWalletClickIntents(), VisaWalletIntents {
private val balancesAndLimitsBottomSheetConverter by lazy(mode = LazyThreadSafetyMode.NONE) {
BalancesAndLimitsBottomSheetConverter(eventSender)
BalancesAndLimitsBottomSheetConverter(eventSender, analyticsEventHandler)
}
override fun onBalancesAndLimitsClick() {
analyticsEventHandler.send(MainScreenAnalyticsEvent.LimitsClicked)
modelScope.launch(dispatchers.main) {
val userWalletId = stateController.getSelectedWalletId()
val balancesAndLimits = getVisaCurrencyUseCase(userWalletId)
@ -76,6 +91,23 @@ internal class VisaWalletIntentsImplementor @Inject constructor(
}
override fun onExploreClick(exploreUrl: String) {
analyticsEventHandler.send(MainScreenAnalyticsEvent.ButtonExplore)
router.openUrl(exploreUrl)
}
override fun onDisputeClick(txDetails: VisaTxDetails) {
modelScope.launch {
val userWalletId = stateController.getSelectedWalletId()
val userWallet = getUserWalletsUseCase.invokeSync()
.firstOrNull { it.walletId == userWalletId } ?: return@launch
val cardInfo = getCardInfoUseCase.invoke(userWallet.scanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(
FeedbackEmailType.Visa.Dispute(
cardInfo = cardInfo,
visaTxDetails = txDetails,
),
)
}
}
}

View file

@ -5,7 +5,6 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.extenstions.unwrap
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.nft.FetchNFTCollectionsUseCase
import com.tangem.domain.onramp.FetchHotCryptoUseCase
import com.tangem.domain.settings.NeverToShowWalletsScrollPreview
import com.tangem.domain.tokens.FetchCardTokenListUseCase
@ -15,7 +14,6 @@ import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
@ -52,8 +50,6 @@ internal class WalletClickIntents @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
private val onrampFeatureToggles: OnrampFeatureToggles,
private val fetchHotCryptoUseCase: FetchHotCryptoUseCase,
private val fetchNFTCollectionsUseCase: FetchNFTCollectionsUseCase,
private val isWalletNFTEnabledSyncUseCase: IsWalletNFTEnabledSyncUseCase,
) : BaseWalletClickIntents(),
WalletCardClickIntents by walletCardClickIntentsImplementor,
WalletWarningsClickIntents by warningsClickIntentsImplementer,
@ -142,10 +138,6 @@ internal class WalletClickIntents @Inject constructor(
async { rampStateManager.fetchSellServiceData() }.let(::add)
async { fetchHotCryptoUseCase() }.let(::add)
if (isWalletNFTEnabledSyncUseCase.invoke(userWallet.walletId)) {
async { fetchNFTCollectionsUseCase(userWalletId = userWallet.walletId) }.let(::add)
}
}
.awaitAll()

View file

@ -2,7 +2,9 @@ package com.tangem.feature.wallet.child.wallet.model.intents
import arrow.core.getOrElse
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.nft.analytics.NFTAnalyticsEvent
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
@ -12,7 +14,6 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
@ -37,7 +38,7 @@ internal interface WalletContentClickIntents {
fun onOrganizeTokensClick()
fun onOnrampSuccessClick(externalTxId: String)
fun onOnrampSuccessClick(txId: String)
fun onDismissMarketsOnboarding()
@ -57,7 +58,7 @@ internal interface WalletContentClickIntents {
fun onDisposeExpressStatus()
fun onNFTClick(userWalletId: UserWalletId)
fun onNFTClick(userWallet: UserWallet)
}
@Suppress("LongParameterList")
@ -75,6 +76,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
private val reduxStateHolder: ReduxStateHolder,
private val walletEventSender: WalletEventSender,
private val analyticsEventHandler: AnalyticsEventHandler,
) : BaseWalletClickIntents(), WalletContentClickIntents {
override fun onDetailsClick() {
@ -114,8 +116,8 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId())
}
override fun onOnrampSuccessClick(externalTxId: String) {
router.openOnrampSuccessScreen(externalTxId = externalTxId)
override fun onOnrampSuccessClick(txId: String) {
router.openOnrampSuccessScreen(txId = txId)
}
override fun onDismissMarketsOnboarding() {
@ -239,7 +241,20 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
stateHolder.update(CloseBottomSheetTransformer(userWalletId))
}
override fun onNFTClick(userWalletId: UserWalletId) {
router.openNFTCollectionsScreen(userWalletId)
override fun onNFTClick(userWallet: UserWallet) {
val selectedWallet = stateHolder.getSelectedWallet() as? WalletState.MultiCurrency.Content ?: return
val analyticsState = when (val nftState = selectedWallet.nftState) {
is WalletNFTItemUM.Content -> NFTAnalyticsEvent.NFTListScreenOpened.State.Full(nftState.assetsCount)
is WalletNFTItemUM.Empty -> NFTAnalyticsEvent.NFTListScreenOpened.State.Empty
is WalletNFTItemUM.Failed,
is WalletNFTItemUM.Hidden,
is WalletNFTItemUM.Loading,
-> null
}
analyticsState?.let {
analyticsEventHandler.send(NFTAnalyticsEvent.NFTListScreenOpened(analyticsState))
}
router.openNFT(userWallet)
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetCo
import com.tangem.common.ui.bottomsheet.receive.AddressModel
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.WrappedList
@ -78,7 +79,7 @@ interface WalletCurrencyActionsClickIntents {
unavailabilityReason: ScenarioUnavailabilityReason,
)
fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent? = null)
fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?)
@ -94,7 +95,7 @@ interface WalletCurrencyActionsClickIntents {
fun onAnalyticsClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onMultiWalletBuyClick(userWalletId: UserWalletId)
fun onMultiWalletBuyClick(userWalletId: UserWalletId, screenType: String)
fun onMultiWalletSellClick(userWalletId: UserWalletId)
@ -151,7 +152,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
appRouter.push(route)
}
override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent?) {
val userWalletId = stateHolder.getSelectedWalletId()
analyticsEventHandler.send(
@ -166,6 +167,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrencyStatus.currency.symbol),
)
event?.let { analyticsEventHandler.send(it) }
stateHolder.showBottomSheet(
createReceiveBottomSheetContent(
currency = cryptoCurrencyStatus.currency,
@ -470,7 +473,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
}
override fun onMultiWalletBuyClick(userWalletId: UserWalletId) {
override fun onMultiWalletBuyClick(userWalletId: UserWalletId, screenType: String) {
onMultiWalletActionClick(
statusFlow = if (onrampFeatureToggles.isFeatureEnabled) {
rampStateManager.getExpressInitializationStatus(userWalletId)
@ -478,7 +481,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
rampStateManager.getBuyInitializationStatus()
},
route = AppRoute.BuyCrypto(userWalletId = userWalletId),
eventCreator = MainScreenAnalyticsEvent::ButtonBuy,
eventCreator = { MainScreenAnalyticsEvent.ButtonBuy(status = it, screenType = screenType) },
)
}

View file

@ -10,6 +10,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
@ -67,13 +68,13 @@ internal class DefaultWalletRouter @Inject constructor(
}
}
override fun openOnrampSuccessScreen(externalTxId: String) {
override fun openOnrampSuccessScreen(txId: String) {
// finish current onramp flow and show onramp success screen
val replaceOnrampScreens = router.stack
.filterNot { it is AppRoute.Onramp }
.toMutableList()
replaceOnrampScreens.add(AppRoute.OnrampSuccess(externalTxId))
replaceOnrampScreens.add(AppRoute.OnrampSuccess(txId))
router.replaceAll(*replaceOnrampScreens.toTypedArray())
}
@ -116,7 +117,12 @@ internal class DefaultWalletRouter @Inject constructor(
reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain))
}
override fun openNFTCollectionsScreen(userWalletId: UserWalletId) {
router.push(AppRoute.NFTCollections(userWalletId))
override fun openNFT(userWallet: UserWallet) {
router.push(
AppRoute.NFT(
userWalletId = userWallet.walletId,
walletName = userWallet.name,
),
)
}
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
@ -33,8 +34,8 @@ internal interface InnerWalletRouter {
/** Open onboarding screen */
fun openOnboardingScreen(scanResponse: ScanResponse, continueBackup: Boolean = false)
/** Open onramp success screen for [externalTxId] */
fun openOnrampSuccessScreen(externalTxId: String)
/** Open onramp success screen for [txId] */
fun openOnrampSuccessScreen(txId: String)
/** Open transaction history website by [url] */
fun openUrl(url: String)
@ -58,5 +59,5 @@ internal interface InnerWalletRouter {
fun openScanFailedDialog(onTryAgain: () -> Unit)
/** Open NFT collections screen */
fun openNFTCollectionsScreen(userWalletId: UserWalletId)
fun openNFT(userWallet: UserWallet)
}

View file

@ -11,6 +11,7 @@ import com.tangem.domain.onramp.OnrampUpdateTransactionStatusUseCase
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.OnrampStatus.Status.*
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -40,22 +41,22 @@ internal class OnrampStatusFactory @Inject constructor(
}
}
suspend fun updateOnrmapTransactionStatuses() = withContext(dispatchers.io) {
suspend fun updateOnrmapTransactionStatuses(userWallet: UserWallet) = withContext(dispatchers.io) {
val singleWalletState = stateHolder.getSelectedWallet() as? WalletState.SingleCurrency.Content
?: return@withContext
singleWalletState.expressTxs.map { tx ->
async {
if (tx is ExpressTransactionStateUM.OnrampUM) {
updateOnrampTxStatus(tx)
updateOnrampTxStatus(userWallet, tx)
}
}
}.awaitAll()
}
private suspend fun updateOnrampTxStatus(onrampTx: ExpressTransactionStateUM.OnrampUM) {
private suspend fun updateOnrampTxStatus(userWallet: UserWallet, onrampTx: ExpressTransactionStateUM.OnrampUM) {
if (!onrampTx.activeStatus.isTerminal) {
getOnrampStatusUseCase(onrampTx.info.txId).fold(
getOnrampStatusUseCase(userWallet = userWallet, onrampTx.info.txId).fold(
ifLeft = {
Timber.e("Couldn't update onramp status. $it")
},
@ -93,10 +94,12 @@ internal class OnrampStatusFactory @Inject constructor(
PaymentProcessing,
Paid,
Sending,
RefundInProgress,
-> ExpressAnalyticsStatus.InProgress
Verifying -> ExpressAnalyticsStatus.KYC
Failed -> ExpressAnalyticsStatus.Fail
Finished -> ExpressAnalyticsStatus.Done
Refunded -> ExpressAnalyticsStatus.Refunded
null -> null
}
}

View file

@ -6,6 +6,7 @@ import kotlinx.collections.immutable.ImmutableList
internal data class VisaTxDetailsBottomSheetConfig(
val transaction: Transaction,
val requests: ImmutableList<Request>,
val onDisputeClick: () -> Unit,
) : TangemBottomSheetConfigContent {
data class Transaction(

View file

@ -1,6 +1,5 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.models.StatusSource
import com.tangem.domain.nft.models.*
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM
@ -16,14 +15,8 @@ internal class SetNFTCollectionsTransformer(
override fun transform(prevState: WalletState): WalletState = when (prevState) {
is WalletState.MultiCurrency.Content -> prevState.copy(
nftState = when {
nftCollections.allCollectionsFailed() ->
WalletNFTItemUM.Failed
nftCollections.anyCollectionFailed() && nftCollections.allLoadedCollectionsEmpty() ->
WalletNFTItemUM.Failed
nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() ->
nftCollections.allLoadedCollectionsEmpty() ->
WalletNFTItemUM.Empty(onItemClick)
!nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() ->
WalletNFTItemUM.Loading
else -> createContentNFTItemUM(onItemClick)
},
)
@ -41,10 +34,6 @@ internal class SetNFTCollectionsTransformer(
.map { it.content }
.filterIsInstance<NFTCollections.Content.Collections>()
val isFlickering = collectionsContent
.map { it.source }
.any { it == StatusSource.CACHE }
val collections = collectionsContent
.mapNotNull { it.collections }
.flatten()
@ -65,7 +54,7 @@ internal class SetNFTCollectionsTransformer(
collectionsCount = collections.size,
assetsCount = collections
.sumOf { it.count },
isFlickering = isFlickering,
isFlickering = false,
onItemClick = onItemClick,
)
}

View file

@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent.Companion.VISA_TYPE
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
@ -144,17 +146,23 @@ internal class SetVisaInfoTransformer(
),
)
return return persistentListOf(
return persistentListOf(
WalletManageButton.Receive(
enabled = true,
dimContent = false,
onClick = { clickIntents.onReceiveClick(cryptoCurrencyStatus = cryptoCurrencyStatus) },
onLongClick = { clickIntents.onCopyAddressLongClick(cryptoCurrencyStatus = cryptoCurrencyStatus) },
onClick = {
clickIntents.onReceiveClick(cryptoCurrencyStatus, MainScreenAnalyticsEvent.ButtonReceive)
},
onLongClick = {
clickIntents.onCopyAddressLongClick(cryptoCurrencyStatus)
},
),
WalletManageButton.Buy(
enabled = true,
dimContent = false,
onClick = { clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId) },
onClick = {
clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId, screenType = VISA_TYPE)
},
),
)
}

View file

@ -1,5 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.DateTimeFormatters
@ -13,6 +15,7 @@ import java.math.BigDecimal
internal class BalancesAndLimitsBottomSheetConverter(
private val eventSender: WalletEventSender,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Converter<VisaCurrency, BalancesAndLimitsBottomSheetConfig> {
override fun convert(value: VisaCurrency): BalancesAndLimitsBottomSheetConfig {
@ -28,23 +31,25 @@ internal class BalancesAndLimitsBottomSheetConverter(
blockedBalance = value.balances.blocked.let(::formatAmount),
debit = value.balances.debt.let(::formatAmount),
amlVerified = value.balances.verified.let(::formatAmount),
onInfoClick = this::showBalanceInfo,
onInfoClick = this::balanceInfoOnClick,
),
limit = BalancesAndLimitsBottomSheetConfig.Limit(
availableBy = DateTimeFormatters.formatDate(date = value.limits.expirationDate),
total = otpLimit,
other = noOtpLimit,
singleTransaction = value.limits.singleTransaction.let(::formatAmount),
onInfoClick = { showLimitInfo(otpLimit, noOtpLimit) },
onInfoClick = { limitInfoOnClick(otpLimit, noOtpLimit) },
),
)
}
private fun showBalanceInfo() {
private fun balanceInfoOnClick() {
analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeBalancesInfo)
eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaBalancesInfo))
}
private fun showLimitInfo(totalLimit: String, otherLimit: String) {
private fun limitInfoOnClick(totalLimit: String, otherLimit: String) {
analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeLimitsInfo)
eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo(totalLimit, otherLimit)))
}
}

View file

@ -1,9 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.common.ui.expressStatus.state.*
import com.tangem.common.ui.notifications.ExpressNotificationsUM
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -22,11 +19,8 @@ import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.feature.wallet.impl.R
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.impl.R
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
@ -142,8 +136,11 @@ internal class SingleWalletOnrampTransactionConverter(
private fun getIconState(status: OnrampStatus.Status): ExpressTransactionStateIconUM {
return when (status) {
OnrampStatus.Status.Verifying -> ExpressTransactionStateIconUM.Warning
OnrampStatus.Status.Failed -> ExpressTransactionStateIconUM.Error
OnrampStatus.Status.Verifying,
-> ExpressTransactionStateIconUM.Warning
OnrampStatus.Status.Refunded,
OnrampStatus.Status.Failed,
-> ExpressTransactionStateIconUM.Error
else -> ExpressTransactionStateIconUM.None
}
}
@ -226,6 +223,10 @@ internal class SingleWalletOnrampTransactionConverter(
wrappedList(currency.name),
)
}
this == OnrampStatus.Status.RefundInProgress ||
this == OnrampStatus.Status.Refunded -> {
resourceReference(R.string.express_exchange_status_failed)
}
else -> {
resourceReference(R.string.express_status_bought, wrappedList(currency.name))
}
@ -247,6 +248,12 @@ internal class SingleWalletOnrampTransactionConverter(
wrappedList(currency.name),
)
}
this == OnrampStatus.Status.RefundInProgress -> {
resourceReference(R.string.express_exchange_status_refunding)
}
this == OnrampStatus.Status.Refunded -> {
resourceReference(R.string.express_exchange_status_refunded)
}
else -> {
resourceReference(
R.string.express_exchange_status_sent,

View file

@ -24,6 +24,7 @@ internal class VisaTxDetailsBottomSheetConverter(
return VisaTxDetailsBottomSheetConfig(
transaction = createTransaction(value),
requests = value.requests.map(::createRequest).toImmutableList(),
onDisputeClick = { clickIntents.onDisputeClick(value) },
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.state.utils
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent.Companion.WALLET_TYPE
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
@ -103,7 +104,7 @@ internal class WalletLoadingStateFactory(
WalletManageButton.Buy(
enabled = true,
dimContent = false,
onClick = { clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId) },
onClick = { clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId, WALLET_TYPE) },
),
WalletManageButton.Swap(
enabled = true,

View file

@ -37,7 +37,7 @@ internal class WalletNFTListSubscriber(
SetNFTCollectionsTransformer(
userWalletId = userWallet.walletId,
nftCollections = it,
onItemClick = { clickIntents.onNFTClick(userWallet.walletId) },
onItemClick = { clickIntents.onNFTClick(userWallet) },
),
)
}

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -15,6 +16,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
@ -36,26 +38,45 @@ internal fun VisaTxDetailsBottomSheet(config: TangemBottomSheetConfig) {
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun VisaTxDetailsBottomSheetContent(config: VisaTxDetailsBottomSheetConfig, modifier: Modifier = Modifier) {
ContentContainer(
modifier = modifier,
blocksCount = config.requests.size.inc(),
title = {
Column {
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size44)
.background(TangemTheme.colors.background.secondary),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResourceSafe(R.string.visa_transaction_details_header),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
},
block = { index ->
if (index == 0) {
}
LazyColumn(
modifier = modifier.background(TangemTheme.colors.background.secondary),
contentPadding = PaddingValues(
bottom = TangemTheme.dimens.spacing16,
),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12),
horizontalAlignment = Alignment.CenterHorizontally,
) {
item {
TransactionBlock(config.transaction)
} else {
BlockchainRequestBlock(config.requests[index - 1])
}
},
)
items(config.requests) { item ->
BlockchainRequestBlock(item)
}
item {
DisputeButton(config.onDisputeClick)
}
}
}
}
@Composable
@ -173,38 +194,14 @@ private fun BlockchainRequestBlock(request: VisaTxDetailsBottomSheetConfig.Reque
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ContentContainer(
blocksCount: Int,
title: @Composable BoxScope.() -> Unit,
block: @Composable ColumnScope.(Int) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier.background(TangemTheme.colors.background.secondary),
contentPadding = PaddingValues(
bottom = TangemTheme.dimens.spacing16,
),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12),
horizontalAlignment = Alignment.CenterHorizontally,
) {
stickyHeader {
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size44)
.background(TangemTheme.colors.background.secondary),
contentAlignment = Alignment.Center,
content = title,
)
}
items(blocksCount) { index ->
Column {
block(index)
}
}
}
private fun DisputeButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
SecondaryButtonIconStart(
modifier = modifier.fillMaxWidth(),
text = stringResourceSafe(R.string.visa_tx_dispute_button),
iconResId = R.drawable.ic_alert_triangle_20,
onClick = onClick,
)
}
// region Preview
@ -249,20 +246,8 @@ private class VisaTxDetailsBottomSheetParameterProvider :
txStatus = "confirmed",
onExploreClick = {},
),
VisaTxDetailsBottomSheetConfig.Request(
id = "524582128501966799",
type = "settlement",
status = "accepted",
blockchainAmount = "1.0614 USDT",
transactionAmount = "0.99 €",
currencyCode = "978",
errorCode = 0,
date = "2023-12-01 00:01:00.000 +0300",
txHash = "0x635841d5fbdf1087cdd929019c863ee88a7165e4340bc17ddd0b1d04dfb11daa",
txStatus = "confirmed",
onExploreClick = {},
),
),
onDisputeClick = {},
),
),
)