From 8db160652cb4bc0ab2ea25ade1943bc53d8ca98c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Oct 2025 12:33:42 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../com/tangem/domain/card/BackupValidator.kt | 46 +++++ features/hot-wallet/impl/build.gradle.kts | 1 + .../DefaultUpgradeWalletComponent.kt | 18 ++ .../upgradewallet/UpgradeWalletModel.kt | 26 +++ .../onboarding/v2/util/ResetCardsComponent.kt | 20 ++ .../entry/impl/model/OnboardingEntryModel.kt | 9 + .../impl/routing/OnboardingChildFactory.kt | 1 + .../v2/entry/impl/routing/OnboardingRoute.kt | 1 + .../api/OnboardingMultiWalletComponent.kt | 1 + .../DefaultOnboardingMultiWalletComponent.kt | 6 + .../finalize/MultiWalletFinalizeComponent.kt | 18 +- .../model/MultiWalletFinalizeModel.kt | 37 ++++ .../child/finalize/ui/MultiWalletFinalize.kt | 1 + .../ui/state/MultiWalletFinalizeUM.kt | 1 + .../util/impl/DefaultResetCardsComponent.kt | 30 +++ .../v2/util/impl/di/ComponentModule.kt | 32 ++++ .../v2/util/impl/model/ResetCardsModel.kt | 174 ++++++++++++++++++ 17 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/BackupValidator.kt create mode 100644 features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/util/ResetCardsComponent.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/DefaultResetCardsComponent.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/di/ComponentModule.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/BackupValidator.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/BackupValidator.kt new file mode 100644 index 0000000000..116b0060b5 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/BackupValidator.kt @@ -0,0 +1,46 @@ +package com.tangem.domain.card + +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.card.configs.CardConfig +import com.tangem.domain.models.scan.CardDTO + +object BackupValidator { + + fun isValidFull(cardDTO: CardDTO): Boolean { + return validateBackupStatus(cardDTO) && validateCurves(cardDTO) + } + + fun isValidBackupStatus(cardDTO: CardDTO): Boolean { + return validateBackupStatus(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 + backupStatus ?: return true // for card with null backup status, validation should always returns true + return backupStatus !is CardDTO.BackupStatus.CardLinked + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index 481078432a..0a0ed7aef6 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { /** Api */ implementation(projects.features.hotWallet.api) + implementation(projects.features.onboardingV2.api) implementation(projects.features.pushNotifications.api) /** Core modules */ diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt index 2f0e91d1de..3775021494 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt @@ -5,21 +5,39 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.hotwallet.UpgradeWalletComponent import com.tangem.features.hotwallet.upgradewallet.ui.UpgradeWalletContent +import com.tangem.features.onboarding.v2.util.ResetCardsComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach @Suppress("UnusedPrivateMember") internal class DefaultUpgradeWalletComponent @AssistedInject constructor( @Assisted private val context: AppComponentContext, @Assisted private val params: UpgradeWalletComponent.Params, + resetCardsComponentFactory: ResetCardsComponent.Factory, ) : UpgradeWalletComponent, AppComponentContext by context { private val model: UpgradeWalletModel = getOrCreateModel(params) + private val resetCardsComponent = resetCardsComponentFactory.create( + context = child("ResetCardsComponent"), + params = ResetCardsComponent.Params( + callbacks = model.resetCardsComponentCallbacks, + ), + ) + + init { + model.startResetCardsFlow + .onEach { resetCardsComponent.startResetCardsFlow(it) } + .launchIn(componentScope) + } + @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt index 3544180c6b..6939c1daf0 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt @@ -16,20 +16,25 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.card.BackupValidator import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.usecase.ClearHotWalletContextualUnlockUseCase import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.features.hotwallet.UpgradeWalletComponent import com.tangem.features.hotwallet.upgradewallet.entity.UpgradeWalletUM +import com.tangem.features.onboarding.v2.util.ResetCardsComponent import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.extensions.localizedDescriptionRes import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -50,9 +55,13 @@ internal class UpgradeWalletModel @Inject constructor( private val clearHotWalletContextualUnlockUseCase: ClearHotWalletContextualUnlockUseCase, private val tangemSdkManager: TangemSdkManager, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, ) : Model() { private val params = paramsContainer.require() + val resetCardsComponentCallbacks = ResetCardsModelCallbacks() + val startResetCardsFlow = MutableSharedFlow() + internal val uiState: StateFlow field = MutableStateFlow( UpgradeWalletUM( @@ -89,6 +98,13 @@ internal class UpgradeWalletModel @Inject constructor( tangemSdkManager .scanProduct() .doOnSuccess { + // Check if user attempted to upgrade before but something went wrong and a full reset is required + val userWallet = coldUserWalletBuilderFactory.create(it).build() + if (userWallet?.walletId == params.userWalletId && BackupValidator.isValidFull(it.card).not()) { + startResetCardsFlow.emit(userWallet) + return@doOnSuccess + } + delay(DELAY_SDK_DIALOG_CLOSE) tangemSdkManager.changeDisplayedCardIdNumbersCount(it) navigateToUpgradeFlow(it) @@ -143,4 +159,14 @@ internal class UpgradeWalletModel @Inject constructor( ), ) } + + inner class ResetCardsModelCallbacks : ResetCardsComponent.ModelCallbacks { + override fun onCancel() { + setLoading(false) + } + + override fun onComplete() { + setLoading(false) + } + } } \ No newline at end of file diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/util/ResetCardsComponent.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/util/ResetCardsComponent.kt new file mode 100644 index 0000000000..bd3a3203e2 --- /dev/null +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/util/ResetCardsComponent.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onboarding.v2.util + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.domain.models.wallet.UserWallet + +interface ResetCardsComponent { + + fun startResetCardsFlow(createdUserWallet: UserWallet.Cold) + + data class Params( + val callbacks: ModelCallbacks, + ) + + interface ModelCallbacks { + fun onCancel() + fun onComplete() + } + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index 3d0f4e7079..279319ac62 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -95,6 +95,7 @@ internal class OnboardingEntryModel @Inject constructor( withSeedPhraseFlow = scanResponse.productType != ProductType.Wallet, titleProvider = titleProvider, onDone = ::onMultiWalletOnboardingDone, + onForceExit = ::popOrExitComponentScreen, mode = multiWalletNavigationMode, ) } @@ -204,6 +205,14 @@ internal class OnboardingEntryModel @Inject constructor( } } + private fun popOrExitComponentScreen() { + router.pop(onComplete = { success -> + if (!success) { + exitComponentScreen() + } + }) + } + private fun exitComponentScreen() { // new flow if (hotWalletFeatureToggles.isHotWalletEnabled) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt index 85e1c6dac5..911596766b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt @@ -36,6 +36,7 @@ internal class OnboardingChildFactory @Inject constructor( withSeedPhraseFlow = route.withSeedPhraseFlow, titleProvider = route.titleProvider, onDone = route.onDone, + onForceExit = route.onForceExit, mode = route.mode, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt index c43edc87ea..a37ce1dd18 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt @@ -25,6 +25,7 @@ sealed class OnboardingRoute : Route { val withSeedPhraseFlow: Boolean, val mode: OnboardingMultiWalletComponent.Mode, val onDone: (UserWallet.Cold) -> Unit, + val onForceExit: () -> Unit, ) : OnboardingRoute() data class Visa( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/api/OnboardingMultiWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/api/OnboardingMultiWalletComponent.kt index 75a31024e6..268966cf50 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/api/OnboardingMultiWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/api/OnboardingMultiWalletComponent.kt @@ -16,6 +16,7 @@ interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavi val withSeedPhraseFlow: Boolean, val mode: Mode, val onDone: (UserWallet.Cold) -> Unit, + val onForceExit: () -> Unit, ) sealed class Mode { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt index 6a1d3cf78e..f13142d4d5 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt @@ -41,6 +41,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiW import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.Step.* import com.tangem.features.onboarding.v2.multiwallet.impl.ui.OnboardingMultiWallet import com.tangem.features.onboarding.v2.multiwallet.impl.ui.WalletArtworksState +import com.tangem.features.onboarding.v2.util.ResetCardsComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -51,6 +52,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor @Assisted private val context: AppComponentContext, @Assisted private val params: OnboardingMultiWalletComponent.Params, private val analyticsHandler: AnalyticsEventHandler, + private val resetCardsComponentFactory: ResetCardsComponent.Factory, ) : OnboardingMultiWalletComponent, AppComponentContext by context { private val model: OnboardingMultiWalletModel = getOrCreateModel(params) @@ -178,6 +180,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor ) Finalize -> MultiWalletFinalizeComponent( context = childContext, + resetCardsComponentFactory = resetCardsComponentFactory, params = childParams, backButtonClickFlow = backButtonClickFlow, onBack = { model.onBack() }, @@ -249,6 +252,9 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor MultiWalletFinalizeComponent.Event.ThreeBackupCardsAdded -> { handleNavigationEvent(Done) } + MultiWalletFinalizeComponent.Event.ExitFromFlow -> { + params.onForceExit() + } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt index 5c8f75423b..824770dc83 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.onboarding.v2.impl.R @@ -13,13 +14,17 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChild import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model.MultiWalletFinalizeModel import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui.MultiWalletFinalize +import com.tangem.features.onboarding.v2.util.ResetCardsComponent import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @Suppress("UnusedPrivateMember") internal class MultiWalletFinalizeComponent( context: AppComponentContext, + resetCardsComponentFactory: ResetCardsComponent.Factory, params: MultiWalletChildParams, backButtonClickFlow: SharedFlow, onBack: () -> Unit, @@ -28,6 +33,13 @@ internal class MultiWalletFinalizeComponent( private val model: MultiWalletFinalizeModel = getOrCreateModel(params) + private val resetCardsComponent = resetCardsComponentFactory.create( + context = child("ResetCardsComponent"), + params = ResetCardsComponent.Params( + callbacks = model.resetCardsModelCallbacks, + ), + ) + init { params.innerNavigation.update { it.copy( @@ -51,6 +63,10 @@ internal class MultiWalletFinalizeComponent( componentScope.launch { model.onBackFlow.collect { onBack() } } + + model.startFullResetFlow + .onEach { resetCardsComponent.startResetCardsFlow(it) } + .launchIn(componentScope) } @Composable @@ -66,6 +82,6 @@ internal class MultiWalletFinalizeComponent( } enum class Event { - OneBackupCardAdded, TwoBackupCardsAdded, ThreeBackupCardsAdded + OneBackupCardAdded, TwoBackupCardsAdded, ThreeBackupCardsAdded, ExitFromFlow } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 65dd32233d..c91d117f06 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -31,6 +31,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.MultiWa import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui.state.MultiWalletFinalizeUM import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.FinalizeStage.* +import com.tangem.features.onboarding.v2.util.ResetCardsComponent import com.tangem.operations.backup.BackupService import com.tangem.sdk.api.BackupServiceHolder import com.tangem.sdk.api.TangemSdkManager @@ -62,6 +63,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( private val onboardingRepository: OnboardingRepository, private val walletsRepository: WalletsRepository, private val uiMessageSender: UiMessageSender, + private val backupValidator: BackupValidator, ) : Model() { private val params = paramsContainer.require() @@ -76,6 +78,8 @@ internal class MultiWalletFinalizeModel @Inject constructor( val uiState = _uiState.asStateFlow() val onBackFlow = MutableSharedFlow() val onEvent = MutableSharedFlow() + val resetCardsModelCallbacks = ResetCardsModelCallbacks() + val startFullResetFlow = MutableSharedFlow() init { modelScope.launch { @@ -231,9 +235,21 @@ internal class MultiWalletFinalizeModel @Inject constructor( private fun finishBackup() { modelScope.launch { + setLoading(true) val scanResponse = params.multiWalletState.value.currentScanResponse val userWalletCreated = createUserWallet(scanResponse) + // Validate wallet before saving + // If something went wrong - start full reset flow + if (walletHasBackupError || !backupValidator.isValidFull(scanResponse.card)) { + startFullResetFlow.emit( + userWalletCreated.copy( + scanResponse = scanResponse.updateScanResponseAfterBackup(), + ), + ) + return@launch + } + val userWallet = when (params.parentParams.mode) { OnboardingMultiWalletComponent.Mode.Onboarding, OnboardingMultiWalletComponent.Mode.ContinueFinalize, @@ -359,4 +375,25 @@ internal class MultiWalletFinalizeModel @Inject constructor( val mask = "$space*$space*$space*$space" return mask + last4 } + + private fun setLoading(isLoading: Boolean) { + _uiState.update { it.copy(isButtonLoading = isLoading) } + } + + inner class ResetCardsModelCallbacks : ResetCardsComponent.ModelCallbacks { + + override fun onCancel() { + modelScope.launch { + onEvent.emit(MultiWalletFinalizeComponent.Event.ExitFromFlow) + } + } + + override fun onComplete() { + modelScope.launch { + onboardingRepository.clearUnfinishedFinalizeOnboarding() + backupServiceHolder.backupService.get()?.discardSavedBackup() + onEvent.emit(MultiWalletFinalizeComponent.Event.ExitFromFlow) + } + } + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/MultiWalletFinalize.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/MultiWalletFinalize.kt index 24a3780233..944b1f2a60 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/MultiWalletFinalize.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/MultiWalletFinalize.kt @@ -72,6 +72,7 @@ internal fun MultiWalletFinalize(state: MultiWalletFinalizeUM, modifier: Modifie text = stringResourceSafe( if (state.isRing) R.string.onboarding_button_backup_ring else R.string.onboarding_button_backup_origin, ), + showProgress = state.isButtonLoading, iconResId = R.drawable.ic_tangem_24, onClick = state.onScanClick, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/state/MultiWalletFinalizeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/state/MultiWalletFinalizeUM.kt index 00387df368..ddbed0afd2 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/state/MultiWalletFinalizeUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/state/MultiWalletFinalizeUM.kt @@ -4,6 +4,7 @@ import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM internal data class MultiWalletFinalizeUM( val step: Step = Step.Primary, + val isButtonLoading: Boolean = false, val isRing: Boolean = false, val scanPrimary: Boolean = true, val cardNumber: String = "", diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/DefaultResetCardsComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/DefaultResetCardsComponent.kt new file mode 100644 index 0000000000..e7d6d281a1 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/DefaultResetCardsComponent.kt @@ -0,0 +1,30 @@ +package com.tangem.features.onboarding.v2.util.impl + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.features.onboarding.v2.util.ResetCardsComponent +import com.tangem.features.onboarding.v2.util.impl.model.ResetCardsModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultResetCardsComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: ResetCardsComponent.Params, +) : ResetCardsComponent, AppComponentContext by context { + + private val model: ResetCardsModel = getOrCreateModel(params) + + override fun startResetCardsFlow(createdUserWallet: UserWallet.Cold) { + model.startResetCardsFlow(createdUserWallet) + } + + @AssistedFactory + interface Factory : ResetCardsComponent.Factory { + override fun create( + context: AppComponentContext, + params: ResetCardsComponent.Params, + ): DefaultResetCardsComponent + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..97f08e8c17 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/di/ComponentModule.kt @@ -0,0 +1,32 @@ +package com.tangem.features.onboarding.v2.util.impl.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onboarding.v2.util.ResetCardsComponent +import com.tangem.features.onboarding.v2.util.impl.DefaultResetCardsComponent +import com.tangem.features.onboarding.v2.util.impl.model.ResetCardsModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindResetCardsComponent(factory: DefaultResetCardsComponent.Factory): ResetCardsComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + @Binds + @IntoMap + @ClassKey(ResetCardsModel::class) + fun provideModel(model: ResetCardsModel): Model +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt new file mode 100644 index 0000000000..5a89688e94 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt @@ -0,0 +1,174 @@ +package com.tangem.features.onboarding.v2.util.impl.model + +import com.tangem.common.CompletionResult +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.card.common.util.getBackupCardsCount +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.util.ResetCardsComponent +import com.tangem.sdk.api.TangemSdkManager +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import javax.inject.Inject +import kotlin.coroutines.resume + +@ModelScoped +internal class ResetCardsModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val tangemSdkManager: TangemSdkManager, + private val uiMessageSender: UiMessageSender, +) : Model() { + + private val params = paramsContainer.require() + private val callbacks = params.callbacks + private val jobHolder = JobHolder() + + fun startResetCardsFlow(wallet: UserWallet.Cold) { + modelScope.launch { + startFullResetFlow(wallet) + }.saveIn(jobHolder) + } + + private suspend fun startFullResetFlow(createdUserWallet: UserWallet.Cold) { + suspendCancellableCoroutine { continuation -> + uiMessageSender.send( + DialogMessage.invoke( + title = resourceReference(R.string.reset_cards_dialog_first_title), + isDismissable = false, + message = resourceReference(R.string.reset_cards_dialog_first_description), + firstActionBuilder = { + cancelAction { + callbacks.onCancel() + onDismissRequest() + continuation.resume(Unit) + } + }, + secondActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.card_settings_action_sheet_reset), + isWarning = true, + onClick = { + modelScope.launch { + repeatUntilTrue { resetPrimaryCard(createdUserWallet) } + continuation.resume(Unit) + onDismissRequest() + startResetBackupCardsFlow(createdUserWallet) + } + }, + ) + }, + onDismissRequest = {}, + ), + ) + } + } + + private suspend fun startResetBackupCardsFlow(createdUserWallet: UserWallet.Cold) { + val backupCardsCount = createdUserWallet.scanResponse.getBackupCardsCount() ?: 0 + + if (backupCardsCount == 0) { + completeResetFlow() + return + } + + repeat(backupCardsCount) { index -> + suspendCancellableCoroutine { continuation -> + uiMessageSender.send( + DialogMessage.invoke( + title = resourceReference(R.string.card_settings_continue_reset_alert_title), + isDismissable = false, + message = resourceReference(R.string.reset_cards_dialog_next_device_description), + firstActionBuilder = { + cancelAction { + callbacks.onCancel() + onDismissRequest() + continuation.resume(Unit) + } + }, + secondActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.card_settings_action_sheet_reset), + isWarning = true, + onClick = { + modelScope.launch { + repeatUntilTrue { resetBackupCard(index + 1, createdUserWallet.walletId) } + onDismissRequest() + if (index == backupCardsCount - 1) { + completeResetFlow() + } + continuation.resume(Unit) + } + }, + ) + }, + onDismissRequest = {}, + ), + ) + } + } + } + + private suspend fun completeResetFlow() { + suspendCancellableCoroutine { continuation -> + uiMessageSender.send( + DialogMessage.invoke( + title = resourceReference(R.string.card_settings_completed_reset_alert_title), + isDismissable = false, + message = resourceReference(R.string.reset_cards_dialog_complete_description), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_done), + onClick = { + modelScope.launch { + onDismissRequest() + callbacks.onComplete() + continuation.resume(Unit) + } + }, + ) + }, + onDismissRequest = {}, + ), + ) + } + } + + private suspend fun resetPrimaryCard(createdUserWallet: UserWallet.Cold): Boolean { + val scanResponse = createdUserWallet.scanResponse + + val result = tangemSdkManager.resetToFactorySettings( + cardId = scanResponse.card.cardId, + allowsRequestAccessCodeFromRepository = true, + ) + + return when (result) { + is CompletionResult.Failure -> false + is CompletionResult.Success -> true + } + } + + private suspend fun resetBackupCard(cardIndex: Int, userWalletId: UserWalletId): Boolean { + val result = tangemSdkManager.resetBackupCard(cardIndex, userWalletId) + return when (result) { + is CompletionResult.Failure -> false + is CompletionResult.Success -> true + } + } + + private inline fun repeatUntilTrue(action: () -> Boolean) { + while (true) { + if (action()) break + } + } +} \ No newline at end of file