Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-27 15:42:04 +00:00
commit fe9c304306
312 changed files with 7537 additions and 6618 deletions

View file

@ -84,6 +84,7 @@ internal class WalletDeepLinksHandler @Inject constructor(
amount = it.baseCurrencyAmount,
destinationAddress = it.depositWalletAddress,
transactionId = it.transactionId,
tag = it.depositWalletAddressTag,
)
}

View file

@ -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")
}
}

View file

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

View file

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

View file

@ -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(),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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