Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-07 18:43:13 +00:00
parent 0ed70b7333
commit 8d16f06c08
16 changed files with 142 additions and 55 deletions

View file

@ -18,4 +18,5 @@ internal data class UserWalletPublicInformation(
val cardsInWallet: Set<String>,
val scanResponse: ScanResponse,
val isMultiCurrency: Boolean,
val hasBackupError: Boolean = false,
)

View file

@ -20,6 +20,7 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation
wallets = emptyList(),
),
),
hasBackupError = hasBackupError,
)
internal fun UserWalletPublicInformation.toUserWallet(): UserWallet {
@ -30,6 +31,7 @@ internal fun UserWalletPublicInformation.toUserWallet(): UserWallet {
cardsInWallet = cardsInWallet,
scanResponse = scanResponse,
isMultiCurrency = isMultiCurrency,
hasBackupError = hasBackupError,
)
}

View file

@ -72,13 +72,14 @@ object OnboardingHelper {
scanResponse: ScanResponse,
accessCode: String? = null,
backupCardsIds: List<String>? = null,
hasBackupError: Boolean = false,
) {
Analytics.setContext(scanResponse)
scope.launch {
when {
// When should save user wallets, then save card without navigate to save wallet screen
store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
proceedWithScanResponse(scanResponse, backupCardsIds)
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
store.dispatchOnMain(
SaveWalletAction.ProvideBackupInfo(
@ -92,7 +93,7 @@ object OnboardingHelper {
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen
tangemSdkManager.canUseBiometry && preferencesStorage.shouldShowSaveUserWalletScreen -> {
proceedWithScanResponse(scanResponse, backupCardsIds)
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
delay(timeMillis = 1_200)
@ -109,7 +110,7 @@ object OnboardingHelper {
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> {
proceedWithScanResponse(scanResponse, backupCardsIds)
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
@ -129,8 +130,13 @@ object OnboardingHelper {
}
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse, backupCardsIds: List<String>?) {
val userWallet = UserWalletBuilder(scanResponse)
private suspend fun proceedWithScanResponse(
scanResponse: ScanResponse,
backupCardsIds: List<String>?,
hasBackupError: Boolean,
) {
val userWallet = UserWalletBuilder(scanResponse = scanResponse)
.hasBackupError(hasBackupError)
.backupCardsIds(backupCardsIds?.toSet())
.build()
.guard {

View file

@ -9,23 +9,23 @@ import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
object Init : OnboardingWalletAction()
object GetToCreateWalletStep : OnboardingWalletAction()
object CreateWallet : OnboardingWalletAction()
data object Init : OnboardingWalletAction()
data object GetToCreateWalletStep : OnboardingWalletAction()
data object CreateWallet : OnboardingWalletAction()
data class WalletWasCreated(
val shouldSendAnalyticsEvent: Boolean,
val result: CompletionResult<CreateProductWalletTaskResponse>,
) : OnboardingWalletAction()
object Done : OnboardingWalletAction()
data object Done : OnboardingWalletAction()
data class FinishOnboarding(val scope: CoroutineScope) : OnboardingWalletAction()
object ResumeBackup : OnboardingWalletAction()
data object ResumeBackup : OnboardingWalletAction()
data class LoadArtwork(val cardArtworkUriForUnfinishedBackup: Uri? = null) : OnboardingWalletAction()
class SetArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
object OnBackPressed : OnboardingWalletAction()
data object OnBackPressed : OnboardingWalletAction()
}
sealed class OnboardingWallet2Action : OnboardingWalletAction() {
@ -49,45 +49,46 @@ sealed class OnboardingWallet2Action : OnboardingWalletAction() {
sealed class BackupAction : Action {
object IntroduceBackup : BackupAction()
object StartBackup : BackupAction()
object SkipBackup : BackupAction()
data object IntroduceBackup : BackupAction()
data object StartBackup : BackupAction()
data object SkipBackup : BackupAction()
object StartAddingPrimaryCard : BackupAction()
object ScanPrimaryCard : BackupAction()
data object ErrorInBackupCard : BackupAction()
data object StartAddingPrimaryCard : BackupAction()
data object ScanPrimaryCard : BackupAction()
/**
* Check for unfinished backup of standard Wallets
* See more GlobalAction.Onboarding.StartForUnfinishedBackup
*/
object CheckForUnfinishedBackup : BackupAction()
data object CheckForUnfinishedBackup : BackupAction()
object StartAddingBackupCards : BackupAction()
object AddBackupCard : BackupAction() {
object Success : BackupAction()
data object StartAddingBackupCards : BackupAction()
data object AddBackupCard : BackupAction() {
data object Success : BackupAction()
data class ChangeButtonLoading(val isLoading: Boolean) : BackupAction()
}
object FinishAddingBackupCards : BackupAction()
data object FinishAddingBackupCards : BackupAction()
object ShowAccessCodeInfoScreen : BackupAction()
object ShowEnterAccessCodeScreen : BackupAction()
data object ShowAccessCodeInfoScreen : BackupAction()
data object ShowEnterAccessCodeScreen : BackupAction()
data class CheckAccessCode(val accessCode: String) : BackupAction()
data class SetAccessCodeError(val error: AccessCodeError?) : BackupAction()
data class SaveFirstAccessCode(val accessCode: String) : BackupAction()
data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction()
object OnAccessCodeDialogClosed : BackupAction()
data object OnAccessCodeDialogClosed : BackupAction()
object PrepareToWritePrimaryCard : BackupAction()
object WritePrimaryCard : BackupAction()
data object PrepareToWritePrimaryCard : BackupAction()
data object WritePrimaryCard : BackupAction()
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction()
object DiscardBackup : BackupAction()
object DiscardSavedBackup : BackupAction()
object ResumeFoundUnfinishedBackup : BackupAction()
data object DiscardBackup : BackupAction()
data object DiscardSavedBackup : BackupAction()
data object ResumeFoundUnfinishedBackup : BackupAction()
data class ResetBackupCard(val cardId: String) : BackupAction()
}

View file

@ -20,6 +20,7 @@ import com.tangem.domain.userwallets.Artwork
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.backup.BackupService
import com.tangem.tap.*
@ -173,6 +174,7 @@ private fun handleWalletAction(action: Action) {
scanResponse = updatedScanResponse,
accessCode = backupState.accessCode,
backupCardsIds = backupState.backupCardIds,
hasBackupError = backupState.hasBackupError,
)
}
}
@ -484,6 +486,10 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
backupService.proceedBackup { result ->
when (result) {
is CompletionResult.Success -> {
val backupValidator = BackupValidator()
if (!backupValidator.isValid(CardDTO(result.data))) {
store.dispatchOnMain(BackupAction.ErrorInBackupCard)
}
if (backupService.currentState == BackupService.State.Finished) {
store.dispatchOnMain(BackupAction.FinishBackup())
} else {

View file

@ -112,6 +112,7 @@ private object BackupReducer {
} else {
state.copy(backupStep = BackupStep.WriteBackupCard(action.cardNumber))
}
is BackupAction.ErrorInBackupCard -> state.copy(hasBackupError = true)
is BackupAction.SkipBackup -> state.copy(backupStep = BackupStep.Finished)
is BackupAction.FinishBackup -> state.copy(backupStep = BackupStep.Finished)
BackupAction.OnAccessCodeDialogClosed -> state.copy(backupStep = BackupStep.AddBackupCards)

View file

@ -70,6 +70,7 @@ data class BackupState(
val canSkipBackup: Boolean = true,
val isInterruptedBackup: Boolean = false,
val showBtnLoading: Boolean = false,
val hasBackupError: Boolean = false,
)
enum class AccessCodeError {

View file

@ -37,6 +37,7 @@ internal class DefaultDerivationsRepositoryTest {
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = ScanResponseMockFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()),
hasBackupError = false,
)
@Test

View file

@ -11,6 +11,7 @@ class UserWalletBuilder(
private val getCardImageUseCase: GetCardImageUseCase = GetCardImageUseCase(),
) {
private var backupCardsIds: Set<String> = emptySet()
private var hasBackupError: Boolean = false
private val CardDTO.isBackupNotAllowed: Boolean
get() = !this.settings.isBackupAllowed
@ -41,6 +42,13 @@ class UserWalletBuilder(
}
}
/**
* Sets if UserWallet has any backup errors (wrong curves etc). Use in onboarding
*/
fun hasBackupError(hasBackupError: Boolean) = this.apply {
this.hasBackupError = hasBackupError
}
suspend fun build(): UserWallet? {
return with(scanResponse) {
UserWalletIdBuilder.scanResponse(scanResponse)
@ -53,6 +61,7 @@ class UserWalletBuilder(
cardsInWallet = backupCardsIds.plus(card.cardId),
scanResponse = this,
isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(),
hasBackupError = hasBackupError,
)
}
}

View file

@ -303,7 +303,7 @@ data class CardDTO(
data class Active(val cardCount: Int) : BackupStatus()
object NoBackup : BackupStatus()
data object NoBackup : BackupStatus()
val isActive: Boolean
get() = this is Active

View file

@ -20,6 +20,7 @@ data class UserWallet(
val artworkUrl: String,
val cardsInWallet: Set<String>,
val isMultiCurrency: Boolean,
val hasBackupError: Boolean,
val scanResponse: ScanResponse, // TODO: Replace with [com.tangem.domain.models.scan.CardDTO]
) {

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",
@ -45,8 +45,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",
@ -58,37 +58,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

@ -14,6 +14,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.repository.PromoRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
@ -36,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
@ -64,7 +66,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
buildList {
addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents)
addCriticalNotifications(cardTypesResolver)
addCriticalNotifications(userWallet)
addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents)
@ -92,7 +94,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

@ -27,15 +27,20 @@ sealed class WalletNotification(val config: NotificationConfig) {
),
) {
object DevCard : Critical(
data object DevCard : Critical(
title = resourceReference(id = R.string.warning_developer_card_title),
subtitle = resourceReference(id = R.string.warning_developer_card_message),
)
object FailedCardValidation : Critical(
data object FailedCardValidation : Critical(
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(