Updated on 2026-08-14
This commit is contained in:
commit
fe9c304306
312 changed files with 7537 additions and 6618 deletions
|
|
@ -84,6 +84,7 @@ internal class WalletDeepLinksHandler @Inject constructor(
|
|||
amount = it.baseCurrencyAmount,
|
||||
destinationAddress = it.depositWalletAddress,
|
||||
transactionId = it.transactionId,
|
||||
tag = it.depositWalletAddressTag,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ sealed class WalletScreenAnalyticsEvent {
|
|||
override val oneTimeEventId: String = id + userWalletId.stringValue
|
||||
}
|
||||
|
||||
object WalletOpened : Basic(event = "Wallet Opened")
|
||||
data object WalletOpened : Basic(event = "Wallet Opened")
|
||||
|
||||
class CardWasScanned(source: AnalyticsParam.ScannedFrom) : Basic(
|
||||
event = "Card Was Scanned",
|
||||
|
|
@ -53,8 +53,8 @@ sealed class WalletScreenAnalyticsEvent {
|
|||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = "Main Screen", event = event, params = params) {
|
||||
|
||||
object ScreenOpened : MainScreen(event = "Screen opened")
|
||||
object WalletSwipe : MainScreen(event = "Wallet Swipe")
|
||||
data object ScreenOpened : MainScreen(event = "Screen opened")
|
||||
data object WalletSwipe : MainScreen(event = "Wallet Swipe")
|
||||
|
||||
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen(
|
||||
event = "Enable Biometric",
|
||||
|
|
@ -66,37 +66,39 @@ sealed class WalletScreenAnalyticsEvent {
|
|||
params = mapOf("Result" to result.value),
|
||||
)
|
||||
|
||||
object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped")
|
||||
object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped")
|
||||
object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked")
|
||||
object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped")
|
||||
data object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped")
|
||||
data object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped")
|
||||
data object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked")
|
||||
data object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped")
|
||||
|
||||
object NetworksUnreachable : MainScreen(event = "Notice - Networks Unreachable")
|
||||
data object NetworksUnreachable : MainScreen(event = "Notice - Networks Unreachable")
|
||||
|
||||
object MissingAddresses : MainScreen(event = "Notice - Missing Addresses")
|
||||
data object MissingAddresses : MainScreen(event = "Notice - Missing Addresses")
|
||||
|
||||
object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions")
|
||||
data object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions")
|
||||
|
||||
object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem")
|
||||
data object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem")
|
||||
|
||||
object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card")
|
||||
data object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card")
|
||||
|
||||
object TestnetCard : MainScreen(event = "Notice - Testnet Card")
|
||||
data object TestnetCard : MainScreen(event = "Notice - Testnet Card")
|
||||
|
||||
object DemoCard : MainScreen(event = "Notice - Demo Card")
|
||||
data object DemoCard : MainScreen(event = "Notice - Demo Card")
|
||||
|
||||
object DevelopmentCard : MainScreen(event = "Notice - Development Card")
|
||||
data object DevelopmentCard : MainScreen(event = "Notice - Development Card")
|
||||
|
||||
object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock")
|
||||
data object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock")
|
||||
|
||||
object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet")
|
||||
data object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet")
|
||||
|
||||
object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics")
|
||||
data object BackupError : MainScreen(event = "Notice - Backup Error")
|
||||
|
||||
object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan")
|
||||
data object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics")
|
||||
|
||||
object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped")
|
||||
data object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan")
|
||||
|
||||
object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
|
||||
data object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped")
|
||||
|
||||
data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
|
|||
is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard
|
||||
is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses
|
||||
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem
|
||||
is WalletNotification.Critical.BackupError -> MainScreen.BackupError
|
||||
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
|
||||
is WalletNotification.Informational.NoAccount,
|
||||
is WalletNotification.Warning.LowSignatures,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.domain
|
||||
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.domain.common.configs.CardConfig
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import javax.inject.Inject
|
||||
|
||||
class BackupValidator @Inject constructor() {
|
||||
|
||||
fun isValid(cardDTO: CardDTO): Boolean {
|
||||
return validateBackupStatus(cardDTO) && validateCurves(cardDTO)
|
||||
}
|
||||
|
||||
private fun validateCurves(cardDTO: CardDTO): Boolean {
|
||||
val config = CardConfig.createConfig(cardDTO)
|
||||
// / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets,
|
||||
// we cannot determine whether this curve is missing due to an error or because the user
|
||||
// did not want to recreate the wallet.
|
||||
val expectedCurves = config.mandatoryCurves
|
||||
.filterNot { it == EllipticCurve.Bls12381G2Aug }
|
||||
val curves = cardDTO.wallets.map { it.curve }
|
||||
for (expectedCurve in expectedCurves) {
|
||||
val cardCurvesCount = curves.count { it == expectedCurve }
|
||||
|
||||
// missing curve
|
||||
if (cardCurvesCount == 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// duplicated curve
|
||||
if (cardCurvesCount > 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun validateBackupStatus(cardDTO: CardDTO): Boolean {
|
||||
val backupStatus = cardDTO.backupStatus
|
||||
return backupStatus != null && backupStatus !is CardDTO.BackupStatus.CardLinked
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
|
||||
private val promoRepository: PromoRepository,
|
||||
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||
private val backupValidator: BackupValidator,
|
||||
) {
|
||||
|
||||
private var readyForRateAppNotification = false
|
||||
|
|
@ -57,7 +58,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
buildList {
|
||||
addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents)
|
||||
|
||||
addCriticalNotifications(cardTypesResolver)
|
||||
addCriticalNotifications(userWallet)
|
||||
|
||||
addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents)
|
||||
|
||||
|
|
@ -85,7 +86,13 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addCriticalNotifications(cardTypesResolver: CardTypesResolver) {
|
||||
private fun MutableList<WalletNotification>.addCriticalNotifications(userWallet: UserWallet) {
|
||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||
addIf(
|
||||
element = WalletNotification.Critical.BackupError,
|
||||
condition = !backupValidator.isValid(userWallet.scanResponse.card) || userWallet.hasBackupError,
|
||||
)
|
||||
|
||||
addIf(
|
||||
element = WalletNotification.Critical.DevCard,
|
||||
condition = !cardTypesResolver.isReleaseFirmwareType(),
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ internal data class BalancesAndLimitsBottomSheetConfig(
|
|||
|
||||
data class Limit(
|
||||
val availableBy: String,
|
||||
val inStore: String,
|
||||
val total: String,
|
||||
val other: String,
|
||||
val singleTransaction: String,
|
||||
val onInfoClick: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -48,19 +48,32 @@ internal sealed interface WalletAlertState {
|
|||
override val isWarningConfirmButton: Boolean = true
|
||||
}
|
||||
|
||||
object WrongCardIsScanned : Basic() {
|
||||
data class VisaLimitsInfo(
|
||||
val totalLimit: String,
|
||||
val otherLimit: String,
|
||||
) : Basic() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference = stringReference(
|
||||
value = "Limits are needed to control costs, improve security, manage risk. " +
|
||||
"You can spend $totalLimit during the week for card payments in shops and " +
|
||||
"$otherLimit for other transactions, e. g. subscriptions or debts.",
|
||||
)
|
||||
override val onConfirmClick: (() -> Unit)? = null
|
||||
}
|
||||
|
||||
data object WrongCardIsScanned : Basic() {
|
||||
override val title: TextReference = resourceReference(R.string.common_warning)
|
||||
override val message: TextReference = resourceReference(R.string.error_wrong_wallet_tapped)
|
||||
override val onConfirmClick: (() -> Unit)? = null
|
||||
}
|
||||
|
||||
object RescanWallets : Basic() {
|
||||
data object RescanWallets : Basic() {
|
||||
override val title: TextReference = resourceReference(R.string.common_attention)
|
||||
override val message: TextReference = resourceReference(R.string.key_invalidated_warning_description)
|
||||
override val onConfirmClick: (() -> Unit)? = null
|
||||
}
|
||||
|
||||
object VisaBalancesInfo : Basic() {
|
||||
data object VisaBalancesInfo : Basic() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference = stringReference(
|
||||
value = "Available balance is actual funds available, considering pending transactions, " +
|
||||
|
|
@ -68,14 +81,4 @@ internal sealed interface WalletAlertState {
|
|||
)
|
||||
override val onConfirmClick: (() -> Unit)? = null
|
||||
}
|
||||
|
||||
object VisaLimitsInfo : Basic() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference = stringReference(
|
||||
value = "Limits are needed to control costs, improve security, manage risk. " +
|
||||
"You can spend 1 000 USDT during the week for card payments in shops and " +
|
||||
"100 USDT for other transactions, e. g. subscriptions or debts.",
|
||||
)
|
||||
override val onConfirmClick: (() -> Unit)? = null
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +36,10 @@ sealed class WalletBottomSheetConfig(
|
|||
),
|
||||
iconResId = R.drawable.ic_locked_24,
|
||||
primaryButtonConfig = ButtonConfig(
|
||||
text = resourceReference(id = R.string.user_wallet_list_unlock_all),
|
||||
text = resourceReference(
|
||||
id = R.string.user_wallet_list_unlock_all_with,
|
||||
formatArgs = wrappedList(resourceReference(R.string.common_biometrics)),
|
||||
),
|
||||
onClick = onUnlockClick,
|
||||
),
|
||||
secondaryButtonConfig = ButtonConfig(
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
title = resourceReference(id = R.string.warning_failed_to_verify_card_title),
|
||||
subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message),
|
||||
)
|
||||
|
||||
data object BackupError : Critical(
|
||||
title = resourceReference(R.string.warning_backup_errors_title),
|
||||
subtitle = resourceReference(R.string.warning_backup_errors_message),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Warning(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ internal class BalancesAndLimitsBottomSheetConverter(
|
|||
decimals = value.decimals,
|
||||
)
|
||||
|
||||
val otpLimit = value.limits.remainingOtp.let(::formatAmount)
|
||||
val noOtpLimit = value.limits.remainingNoOtp.let(::formatAmount)
|
||||
|
||||
return BalancesAndLimitsBottomSheetConfig(
|
||||
balance = BalancesAndLimitsBottomSheetConfig.Balance(
|
||||
totalBalance = value.balances.total.let(::formatAmount),
|
||||
|
|
@ -33,10 +36,10 @@ internal class BalancesAndLimitsBottomSheetConverter(
|
|||
),
|
||||
limit = BalancesAndLimitsBottomSheetConfig.Limit(
|
||||
availableBy = DateTimeFormatters.formatDate(date = value.limits.expirationDate),
|
||||
inStore = value.limits.remainingOtp.let(::formatAmount),
|
||||
other = value.limits.remainingNoOtp.let(::formatAmount),
|
||||
total = otpLimit,
|
||||
other = noOtpLimit,
|
||||
singleTransaction = value.limits.singleTransaction.let(::formatAmount),
|
||||
onInfoClick = this::showLimitInfo,
|
||||
onInfoClick = { showLimitInfo(otpLimit, noOtpLimit) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -45,7 +48,7 @@ internal class BalancesAndLimitsBottomSheetConverter(
|
|||
eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaBalancesInfo))
|
||||
}
|
||||
|
||||
private fun showLimitInfo() {
|
||||
eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo))
|
||||
private fun showLimitInfo(totalLimit: String, otherLimit: String) {
|
||||
eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo(totalLimit, otherLimit)))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
|
||||
|
||||
import com.tangem.core.ui.extensions.capitalize
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.visa.model.VisaCurrency
|
||||
|
|
@ -27,14 +28,14 @@ internal class VisaTxDetailsBottomSheetConverter(
|
|||
private fun createTransaction(details: VisaTxDetails): VisaTxDetailsBottomSheetConfig.Transaction {
|
||||
return VisaTxDetailsBottomSheetConfig.Transaction(
|
||||
id = details.id,
|
||||
type = details.type,
|
||||
status = details.status,
|
||||
type = details.type.capitalize(),
|
||||
status = details.status.capitalize(),
|
||||
blockchainAmount = formatNetworkAmount(details.blockchainAmount),
|
||||
blockchainFee = formatNetworkAmount(details.blockchainFee),
|
||||
transactionAmount = formatFiatAmount(details.transactionAmount, details.fiatCurrency),
|
||||
transactionCurrencyCode = details.transactionCurrencyCode.toString(),
|
||||
merchantName = details.merchantName ?: UNKNOWN,
|
||||
merchantCity = details.merchantCity ?: UNKNOWN,
|
||||
merchantName = details.merchantName?.capitalize() ?: UNKNOWN,
|
||||
merchantCity = details.merchantCity?.capitalize() ?: UNKNOWN,
|
||||
merchantCountryCode = details.merchantCountryCode ?: UNKNOWN,
|
||||
merchantCategoryCode = details.merchantCategoryCode ?: UNKNOWN,
|
||||
)
|
||||
|
|
@ -46,16 +47,16 @@ internal class VisaTxDetailsBottomSheetConverter(
|
|||
|
||||
return VisaTxDetailsBottomSheetConfig.Request(
|
||||
id = request.id,
|
||||
type = request.requestType,
|
||||
status = request.requestStatus,
|
||||
type = request.requestType.capitalize(),
|
||||
status = request.requestStatus.capitalize(),
|
||||
blockchainAmount = formatNetworkAmount(request.blockchainAmount),
|
||||
blockchainFee = formatNetworkAmount(request.blockchainFee),
|
||||
transactionAmount = formatFiatAmount(request.transactionAmount, request.fiatCurrency),
|
||||
currencyCode = request.billingCurrencyCode.toString(),
|
||||
errorCode = request.errorCode,
|
||||
date = DateTimeFormatters.formatDate(DateTimeFormatters.dateTimeFormatter, date = localDate),
|
||||
date = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.dateTimeFormatter),
|
||||
txHash = request.txHash ?: UNKNOWN,
|
||||
txStatus = request.txStatus ?: UNKNOWN,
|
||||
txStatus = request.txStatus?.capitalize() ?: UNKNOWN,
|
||||
onExploreClick = if (exploreUrl != null) {
|
||||
{ clickIntents.onExploreClick(exploreUrl) }
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
|
||||
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.extensions.capitalize
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
|
|
@ -18,8 +19,8 @@ internal class VisaTxHistoryItemStateConverter(
|
|||
|
||||
override fun convert(value: VisaTxHistoryItem): TransactionState {
|
||||
val localDate = value.date.withZone(DateTimeZone.getDefault())
|
||||
val time = DateTimeFormatters.formatTime(time = localDate)
|
||||
val subtitle = "$time • ${value.status}"
|
||||
val time = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.timeFormatter)
|
||||
val subtitle = "$time • ${value.status.capitalize()}"
|
||||
|
||||
return TransactionState.Content(
|
||||
txHash = value.id,
|
||||
|
|
@ -37,7 +38,7 @@ internal class VisaTxHistoryItemStateConverter(
|
|||
status = TransactionState.Content.Status.Confirmed,
|
||||
direction = TransactionState.Content.Direction.INCOMING,
|
||||
iconRes = R.drawable.ic_arrow_up_24,
|
||||
title = stringReference(value = value.merchantName ?: "Unknown merchant"),
|
||||
title = stringReference(value = value.merchantName?.capitalize() ?: "Unknown merchant"),
|
||||
subtitle = stringReference(subtitle),
|
||||
timestamp = localDate.millis,
|
||||
onClick = { clickIntents.onVisaTransactionClick(value.id) },
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ internal class VisaWalletSubscriber(
|
|||
setLoadedCurrencyState(maybeCurrency)
|
||||
|
||||
val currency = maybeCurrency.getOrElse {
|
||||
Timber.e(it, "Failed to load VISA currency")
|
||||
setFailedTxHistoryState(it)
|
||||
return@flow
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import androidx.compose.animation.AnimatedContent
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -60,7 +63,6 @@ private fun BalancesAndLimitsBlock(state: BalancesAndLimitsBlockState, modifier:
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private inline fun ContentContainer(
|
||||
enabled: Boolean,
|
||||
|
|
@ -147,7 +149,7 @@ private fun AvailableLimit(availableBalance: String, limitDays: Int, modifier: M
|
|||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = "available $limitDays-day limit",
|
||||
text = "available for $limitDays day(s)",
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ private fun LimitsBlock(limits: BalancesAndLimitsBottomSheetConfig.Limit, modifi
|
|||
title = stringReference("Limits"),
|
||||
content = {
|
||||
BlockItem(
|
||||
title = stringReference("In-store (otp)"),
|
||||
value = limits.inStore,
|
||||
title = stringReference("Total"),
|
||||
value = limits.total,
|
||||
)
|
||||
BlockItem(
|
||||
title = stringReference("Other (no-otp)"),
|
||||
|
|
@ -193,7 +193,7 @@ private class BalancesAndLimitsBottomSheetParameterProvider :
|
|||
),
|
||||
limit = BalancesAndLimitsBottomSheetConfig.Limit(
|
||||
availableBy = "Nov, 11 USDT",
|
||||
inStore = "563.00 USDT",
|
||||
total = "563.00 USDT",
|
||||
other = "100.00 USDT",
|
||||
singleTransaction = "100.00 USDT",
|
||||
onInfoClick = {},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue