Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-18 14:36:28 +03:00
commit 0e11a8512f
17 changed files with 111 additions and 93 deletions

View file

@ -82,7 +82,6 @@ object OnboardingHelper {
return AppRoute.Onboarding(
scanResponse = scanResponse,
startFromBackup = false,
mode = when (backupState.startedSource) {
BackupStartedSource.Onboarding -> AppRoute.Onboarding.Mode.Onboarding
BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackup
@ -92,11 +91,7 @@ object OnboardingHelper {
return when (val type = scanResponse.productType) {
ProductType.Note -> if (featureToggles.isNoteRefactoringEnabled) {
AppRoute.Onboarding(
scanResponse = scanResponse,
startFromBackup = false,
mode = AppRoute.Onboarding.Mode.Onboarding,
)
AppRoute.Onboarding(scanResponse = scanResponse)
} else {
AppRoute.OnboardingNote
}

View file

@ -585,7 +585,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
is BackupAction.DiscardSavedBackup -> {
mainScope.launch {
backupService.primaryCardId?.let {
backupService.primaryCardId?.let { // always null for onboarding v2
Analytics.send(Onboarding.Finished())
store.state.globalState.onboardingState.onboardingManager?.finishActivation(it)
}
@ -596,7 +596,12 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
if (isOnboardingV2Enabled) {
val onboardingRepository = store.inject(DaggerGraphState::onboardingRepository)
val cardRepository = store.inject(DaggerGraphState::cardRepository)
val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
cardRepository.finishCardActivation(unfinishedBackup.card.cardId)
onboardingRepository.clearUnfinishedFinalizeOnboarding()
Analytics.send(Onboarding.Finished())
}
}
}
@ -618,12 +623,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
if (action.unfinishedBackupScanResponse != null) {
// onboarding V2
store.dispatchNavigationAction {
push(
AppRoute.Onboarding(
scanResponse = action.unfinishedBackupScanResponse,
startFromBackup = false,
),
)
push(AppRoute.Onboarding(scanResponse = action.unfinishedBackupScanResponse))
}
} else {
store.dispatch(

View file

@ -202,7 +202,6 @@ internal class ChildFactory @Inject constructor(
contextProvider = contextProvider(route, contextFactory),
params = OnboardingEntryComponent.Params(
scanResponse = route.scanResponse,
startBackupFlow = route.startFromBackup,
multiWalletMode = when (route.mode) {
AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding
AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup
@ -641,7 +640,6 @@ internal class ChildFactory @Inject constructor(
contextProvider = contextProvider(route, contextFactory),
params = OnboardingEntryComponent.Params(
scanResponse = route.scanResponse,
startBackupFlow = route.startFromBackup,
multiWalletMode = when (route.mode) {
AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding
AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup

View file

@ -245,15 +245,13 @@ sealed class AppRoute(val path: String) : Route {
/**
* Onboarding V2
* @property scanResponse scan response, determines onboarding route by the product type
* @property startFromBackup (MultiWallet param, doesn't affect other types) start onboarding from backup
* @property mode (MultiWallet param, doesn't affect other types) onboarding mode
*/
@Serializable
data class Onboarding(
val scanResponse: ScanResponse,
val startFromBackup: Boolean = false,
val mode: Mode = Mode.Onboarding,
) : AppRoute(path = "/onboarding_v2${if (startFromBackup) "/backup" else ""}") {
) : AppRoute(path = "/onboarding_v2/${mode.name}") {
enum class Mode {
Onboarding, // general Mode

View file

@ -66,15 +66,6 @@ internal class AskBiometryModel @Inject constructor(
}
}
private fun onAllowClick() {
_uiState.update { it.copy(showProgress = true) }
allowToUseBiometrics()
}
private fun dontAllow() {
params.modelCallbacks.onDenied()
}
fun dismiss() {
modelScope.launch {
dismissBSFlow.emit(Unit)
@ -83,13 +74,15 @@ internal class AskBiometryModel @Inject constructor(
}
}
private fun allowToUseBiometrics() {
private fun onAllowClick() {
modelScope.launch {
if (tangemSdkManager.checkNeedEnrollBiometrics()) {
showEnrollBiometricsDialog()
return@launch
}
_uiState.update { it.copy(showProgress = true) }
/*
* because it will be automatically saved on UserWalletsListManager switch
@ -99,6 +92,7 @@ internal class AskBiometryModel @Inject constructor(
uiMessageSender.send(
SnackbarMessage(stringReference("No selected user wallet")),
)
_uiState.update { it.copy(showProgress = false) }
return@launch
}
@ -107,6 +101,10 @@ internal class AskBiometryModel @Inject constructor(
}
}
private fun dontAllow() {
params.modelCallbacks.onDenied()
}
private suspend fun handleSuccessAllowing(userWallet: UserWallet) {
walletsRepository.saveShouldSaveUserWallets(item = true)
settingsRepository.setShouldSaveAccessCodes(value = true)

View file

@ -17,8 +17,8 @@ import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.biometry.impl.ui.state.AskBiometryUM
import com.tangem.features.biometry.R
import com.tangem.features.biometry.impl.ui.state.AskBiometryUM
@Composable
internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) {

View file

@ -8,7 +8,6 @@ interface OnboardingEntryComponent : ComposableContentComponent {
data class Params(
val scanResponse: ScanResponse,
val startBackupFlow: Boolean,
val multiWalletMode: MultiWalletMode,
)

View file

@ -0,0 +1,14 @@
package com.tangem.features.onboarding.v2.common.ui
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.features.onboarding.v2.impl.R
internal val CantLeaveBackupDialog: DialogMessage = DialogMessage(
message = resourceReference(R.string.onboarding_backup_exit_warning),
firstAction = EventMessageAction(
title = resourceReference(R.string.warning_button_ok),
onClick = {},
),
)

View file

@ -53,7 +53,7 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
popBack = {
popInternal { success ->
if (success.not()) {
model.stackNavigation.pop()
model.onBack()
}
}
},
@ -113,15 +113,15 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
stepperComponent.state.update {
it.copy(
currentStep = when (stack.active.configuration) {
is OnboardingRoute.AskBiometry -> 7
is OnboardingRoute.ManageTokens -> 8
is OnboardingRoute.ManageTokens -> 7
is OnboardingRoute.AskBiometry -> 8
is OnboardingRoute.Done -> 9
else -> error("Unsupported route")
},
steps = 9,
title = when (stack.active.configuration) {
is OnboardingRoute.AskBiometry -> resourceReference(R.string.onboarding_navbar_save_wallet)
is OnboardingRoute.ManageTokens -> resourceReference(R.string.main_manage_tokens)
is OnboardingRoute.AskBiometry -> resourceReference(R.string.onboarding_navbar_save_wallet)
is OnboardingRoute.Done -> resourceReference(R.string.onboarding_done_header)
else -> error("Unsupported route")
},
@ -144,8 +144,7 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
override fun Content(modifier: Modifier) {
val innerStackState by innerStack.subscribeAsState()
// prevent back navigation
BackHandler { }
BackHandler { model.onBack() }
OnboardingEntry(
modifier = modifier,

View file

@ -8,6 +8,7 @@ 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.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.common.util.cardTypesResolver
@ -18,6 +19,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.BiometryFeatureToggles
import com.tangem.features.onboarding.v2.TitleProvider
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onboarding.v2.entry.impl.analytics.OnboardingEntryEvent
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
@ -40,6 +42,7 @@ internal class OnboardingEntryModel @Inject constructor(
private val settingsRepository: SettingsRepository,
private val askBiometryFeatureToggles: BiometryFeatureToggles,
private val analyticsEventHandler: AnalyticsEventHandler,
private val uiMessageSender: UiMessageSender,
) : Model() {
private val params = paramsContainer.require<OnboardingEntryComponent.Params>()
@ -54,6 +57,14 @@ internal class OnboardingEntryModel @Inject constructor(
val startRoute = routeByProductType(params.scanResponse)
fun onManageTokensDone() {
navigateToFinalScreenFlow()
}
fun onBack() {
uiMessageSender.send(CantLeaveBackupDialog)
}
private fun routeByProductType(scanResponse: ScanResponse): OnboardingRoute {
val multiWalletNavigationMode = when (params.multiWalletMode) {
OnboardingEntryComponent.MultiWalletMode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding
@ -92,17 +103,19 @@ internal class OnboardingEntryModel @Inject constructor(
}
private fun onMultiWalletOnboardingDone(userWallet: UserWallet) {
if (userWallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) {
stackNavigation.replaceAll(OnboardingRoute.ManageTokens(userWallet))
} else {
navigateToFinalScreenFlow()
when {
params.multiWalletMode == OnboardingEntryComponent.MultiWalletMode.AddBackup -> {
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
userWallet.scanResponse.cardTypesResolver.isMultiwalletAllowed() -> {
stackNavigation.replaceAll(OnboardingRoute.ManageTokens(userWallet))
}
else -> {
navigateToFinalScreenFlow()
}
}
}
fun onManageTokensDone() {
navigateToFinalScreenFlow()
}
private fun onVisaOnboardingDone() {
navigateToFinalScreenFlow()
}

View file

@ -14,6 +14,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
@ -49,6 +50,7 @@ class MultiWalletBackupModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val uiMessageSender: UiMessageSender,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val cardRepository: CardRepository,
) : Model() {
@Suppress("UnusedPrivateMember")
@ -275,6 +277,8 @@ class MultiWalletBackupModel @Inject constructor(
cardId = cardId,
allowsRequestAccessCodeFromRepository = false,
)
cardRepository.finishCardActivation(cardId)
}
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.common.core.TangemSdkError
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.domain.card.repository.CardRepository
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.feedback.GetCardInfoUseCase
@ -19,6 +20,7 @@ import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
@ -51,6 +53,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
private val cardRepository: CardRepository,
private val onboardingRepository: OnboardingRepository,
private val walletsRepository: WalletsRepository,
private val uiMessageSender: UiMessageSender,
) : Model() {
private val params = paramsContainer.require<MultiWalletChildParams>()
@ -61,35 +64,22 @@ internal class MultiWalletFinalizeModel @Inject constructor(
private var walletHasBackupError = false
private var hasRing = false
val uiState = _uiState.asStateFlow()
val onBackFlow = MutableSharedFlow<Unit>()
val onEvent = MutableSharedFlow<MultiWalletFinalizeComponent.Event>()
init {
// save scan response to preferences to be able
// to continue finalize process after app restart
modelScope.launch {
// save scan response to preferences to be able
// to continue finalize process after app restart
onboardingRepository.saveUnfinishedFinalizeOnboarding(
scanResponse = multiWalletState.value.currentScanResponse,
)
}
}
fun onBack() {
if (uiState.value.scanPrimary) {
modelScope.launch { onBackFlow.emit(Unit) }
}
}
private fun getInitialState(): MultiWalletFinalizeUM {
val backupService = backupServiceHolder.backupService.get() ?: return MultiWalletFinalizeUM()
val initialStep = getInitialStep()
// sets proper artwork state for initial step
// (if we start from backup cards, we need to show proper artwork) ([REDACTED_TASK_KEY])
modelScope.launch {
when (initialStep) {
// sets proper artwork state for initial step
// (if we start from backup cards, we need to show proper artwork) ([REDACTED_TASK_KEY])
when (getInitialStep()) {
MultiWalletFinalizeUM.Step.Primary -> { /* state is already set */ }
MultiWalletFinalizeUM.Step.BackupDevice1 -> {
onEvent.emit(MultiWalletFinalizeComponent.Event.OneBackupCardAdded)
@ -100,6 +90,19 @@ internal class MultiWalletFinalizeModel @Inject constructor(
}
}
}
}
fun onBack() {
if (uiState.value.scanPrimary) {
modelScope.launch { onBackFlow.emit(Unit) }
} else {
uiMessageSender.send(CantLeaveBackupDialog)
}
}
private fun getInitialState(): MultiWalletFinalizeUM {
val backupService = backupServiceHolder.backupService.get() ?: return MultiWalletFinalizeUM()
val initialStep = getInitialStep()
val batchId = when (initialStep) {
MultiWalletFinalizeUM.Step.Primary -> backupService.primaryCardBatchId
@ -118,7 +121,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
onScanClick = ::onLinkClick,
scanPrimary = initialStep == MultiWalletFinalizeUM.Step.Primary,
cardNumber = cardId?.lastMasked().orEmpty(),
step = getInitialStep(),
step = initialStep,
)
}

View file

@ -5,10 +5,9 @@ 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.navigation.Router
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
@ -34,7 +33,7 @@ internal class OnboardingMultiWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val backupServiceHolder: BackupServiceHolder,
private val appPreferencesStore: AppPreferencesStore,
private val onboardingRepository: OnboardingRepository,
private val getCardImageUseCase: GetCardImageUseCase,
) : Model() {
private val params = paramsContainer.require<OnboardingMultiWalletComponent.Params>()
@ -65,8 +64,10 @@ internal class OnboardingMultiWalletModel @Inject constructor(
st.copy(
dialog = interruptBackupDialog(
onConfirm = {
removeFinalizeScanResponseState()
router.pop()
modelScope.launch {
onboardingRepository.clearUnfinishedFinalizeOnboarding()
router.pop()
}
},
dismiss = { _uiState.update { it.copy(dialog = null) } },
),
@ -74,16 +75,6 @@ internal class OnboardingMultiWalletModel @Inject constructor(
}
}
private fun removeFinalizeScanResponseState() {
// discarding onboarding means we have to remove scan response from preferences
// to prevent showing finalize screen dialog on next app start
modelScope.launch {
appPreferencesStore.editData { mutablePreferences ->
mutablePreferences.remove(PreferencesKeys.ONBOARDING_FINALIZE_SCAN_RESPONSE_KEY)
}
}
}
private fun subscribeToBackups() {
modelScope.launch {
backups.collectLatest { backup ->
@ -110,12 +101,8 @@ internal class OnboardingMultiWalletModel @Inject constructor(
private fun getInitialStep(): OnboardingMultiWalletState.Step {
val scanResponse = params.scanResponse
val card = scanResponse.card
val backupService = backupServiceHolder.backupService.get()!!
return when {
// interrupted backup
backupService.hasIncompletedBackup -> OnboardingMultiWalletState.Step.Finalize
// Add backup button
// Wallet1 without backup and userwallet's scanResponse doesn't contain primary card.
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup &&

View file

@ -24,7 +24,7 @@ data class OnboardingMultiWalletState(
* -> [Done]
*
* Wallet2/Ring
* CreateWallet -> SeedPhrase -> AddBackupDevice -> FinishBackup -> [Done]
* CreateWallet -> SeedPhrase -> AddBackupDevice -> Finalize -> [Done]
* |
* -> AddBackupDevice -> Finalize -> [Done]
*/

View file

@ -17,6 +17,7 @@ dependencies {
implementation(projects.features.walletSettings.api)
implementation(projects.features.manageTokens.api)
implementation(projects.features.nft.api)
implementation(projects.features.onboardingV2.api)
/* Project - Core */
implementation(projects.core.decompose)

View file

@ -33,6 +33,7 @@ import com.tangem.feature.walletsettings.entity.WalletSettingsUM
import com.tangem.feature.walletsettings.impl.R
import com.tangem.feature.walletsettings.utils.ItemsBuilder
import com.tangem.features.nft.NFTFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
@ -57,6 +58,7 @@ internal class WalletSettingsModel @Inject constructor(
private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase,
private val walletsRepository: WalletsRepository,
private val nftFeatureToggles: NFTFeatureToggles,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
) : Model() {
val params: WalletSettingsComponent.Params = paramsContainer.require()
@ -156,17 +158,25 @@ internal class WalletSettingsModel @Inject constructor(
private fun onLinkMoreCardsClick(scanResponse: ScanResponse) {
analyticsEventHandler.send(Settings.ButtonCreateBackup)
analyticsContextProxy.addContext(scanResponse)
reduxStateHolder.dispatch(
LegacyAction.StartOnboardingProcess(
scanResponse = scanResponse,
canSkipBackup = false,
),
)
if (onboardingV2FeatureToggles.isOnboardingV2Enabled) {
router.push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.AddBackup,
),
)
} else {
reduxStateHolder.dispatch(
LegacyAction.StartOnboardingProcess(
scanResponse = scanResponse,
canSkipBackup = false,
),
)
router.push(AppRoute.OnboardingWallet())
router.push(AppRoute.OnboardingWallet())
}
}
private fun onCheckedNFTChange(isChecked: Boolean) {

View file

@ -53,7 +53,6 @@ internal class DefaultWalletRouter @Inject constructor(
router.push(
AppRoute.Onboarding(
scanResponse = scanResponse,
startFromBackup = true,
mode = if (continueBackup) {
AppRoute.Onboarding.Mode.AddBackup
} else {