Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-15 13:09:01 +03:00
commit 68ee4b3c00
316 changed files with 7635 additions and 1625 deletions

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

@ -240,6 +240,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
}
override fun onNFTClick(userWalletId: UserWalletId) {
router.openNFTCollectionsScreen(userWalletId)
router.openNFT(userWalletId)
}
}

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

@ -116,7 +116,7 @@ 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(userWalletId: UserWalletId) {
router.push(AppRoute.NFT(userWalletId))
}
}

View file

@ -58,5 +58,5 @@ internal interface InnerWalletRouter {
fun openScanFailedDialog(onTryAgain: () -> Unit)
/** Open NFT collections screen */
fun openNFTCollectionsScreen(userWalletId: UserWalletId)
fun openNFT(userWalletId: UserWalletId)
}

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

@ -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

@ -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

@ -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 = {},
),
),
)