Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-27 12:28:17 +03:00
parent 80d00dfb6b
commit 80d661ab55
26 changed files with 771 additions and 138 deletions

View file

@ -235,13 +235,25 @@ internal class LegacyScanProcessor @Inject constructor(
} else {
Analytics.setContext(scanResponse)
val isTwinRefactoringEnabled =
store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled
val wasTwinsOnboardingShown =
store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync()
if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) {
onWalletNotCreated()
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) }
if (isTwinRefactoringEnabled) {
navigateTo(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin,
),
) { onProgressStateChange(it) }
} else {
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) }
}
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)

View file

@ -59,15 +59,27 @@ class CheckForOnboardingChain(
else -> {
Analytics.setContext(previousChainResult)
val isTwinRefactoringEnabled =
store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
.invokeSync()
// If twins was twinned previously but twins welcome not shown
if (previousChainResult.twinsIsTwinned() && !wasTwinsOnboardingShown) {
store.dispatchOnMain(
TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)),
)
ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left()
if (isTwinRefactoringEnabled) {
ScanChainException.OnboardingNeeded(
AppRoute.Onboarding(
scanResponse = previousChainResult,
mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin,
),
).left()
} else {
store.dispatchOnMain(
TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)),
)
ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left()
}
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
previousChainResult.right()

View file

@ -85,7 +85,7 @@ object OnboardingHelper {
scanResponse = scanResponse,
mode = when (backupState.startedSource) {
BackupStartedSource.Onboarding -> AppRoute.Onboarding.Mode.Onboarding
BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackup
BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackupWallet1
},
)
}

View file

@ -210,9 +210,12 @@ internal class ChildFactory @Inject constructor(
contextProvider = contextProvider(route, contextFactory),
params = OnboardingEntryComponent.Params(
scanResponse = route.scanResponse,
multiWalletMode = when (route.mode) {
AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding
AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup
mode = when (route.mode) {
AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.Mode.Onboarding
AppRoute.Onboarding.Mode.AddBackupWallet1 -> OnboardingEntryComponent.Mode.AddBackupWallet1
AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin
AppRoute.Onboarding.Mode.RecreateWalletTwin ->
OnboardingEntryComponent.Mode.RecreateWalletTwin
},
),
componentFactory = onboardingEntryComponentFactory,
@ -702,9 +705,12 @@ internal class ChildFactory @Inject constructor(
contextProvider = contextProvider(route, contextFactory),
params = OnboardingEntryComponent.Params(
scanResponse = route.scanResponse,
multiWalletMode = when (route.mode) {
AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding
AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup
mode = when (route.mode) {
AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.Mode.Onboarding
AppRoute.Onboarding.Mode.AddBackupWallet1 -> OnboardingEntryComponent.Mode.AddBackupWallet1
AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin
AppRoute.Onboarding.Mode.RecreateWalletTwin ->
OnboardingEntryComponent.Mode.RecreateWalletTwin
},
),
componentFactory = onboardingEntryComponentFactory,

View file

@ -260,7 +260,9 @@ sealed class AppRoute(val path: String) : Route {
enum class Mode {
Onboarding, // general Mode
AddBackup, // continue backup process for existing wallet 1
AddBackupWallet1, // continue backup process for existing wallet 1
WelcomeOnlyTwin, // show welcome screen and then navigate to wallet for twins
RecreateWalletTwin, // reset twins
}
}

View file

@ -8,12 +8,14 @@ interface OnboardingEntryComponent : ComposableContentComponent {
data class Params(
val scanResponse: ScanResponse,
val multiWalletMode: MultiWalletMode,
val mode: Mode,
)
enum class MultiWalletMode {
enum class Mode {
Onboarding,
AddBackup,
AddBackupWallet1,
WelcomeOnlyTwin,
RecreateWalletTwin,
}
interface Factory : ComponentFactory<Params, OnboardingEntryComponent>

View file

@ -77,6 +77,7 @@ dependencies {
implementation(deps.kotlin.serialization)
implementation(deps.timber)
implementation(deps.firebase.crashlytics)
implementation(tangemDeps.card.core)
/** DI */
implementation(deps.hilt.android)

View file

@ -92,4 +92,22 @@ sealed class OnboardingEvent(
params = mapOf("Action" to "Cancel"),
)
}
sealed class Twins(
event: String,
params: Map<String, String> = mapOf(),
) : OnboardingEvent("Onboarding / Twins", event, params) {
data object ScreenOpened : Twins("Twinning Screen Opened")
data object SetupStarted : Twins("Twin Setup Started")
data object SetupFinished : Twins("Twin Setup Finished")
}
data class OfflineAttestationFailed(
val source: AnalyticsParam.ScreensSources,
) : OnboardingEvent(
category = "Error",
event = "Offline Attestation Failed",
params = mapOf(AnalyticsParam.SOURCE to source.value),
)
}

View file

@ -22,4 +22,18 @@ internal fun exitOnboardingDialog(onConfirm: () -> Unit): DialogMessage {
)
},
)
}
}
internal fun interruptBackupDialog(onConfirm: () -> Unit) = DialogMessage(
title = resourceReference(R.string.onboarding_exit_alert_title),
message = resourceReference(R.string.onboarding_exit_alert_message),
firstAction = EventMessageAction(
title = resourceReference(R.string.common_ok),
warning = true,
onClick = onConfirm,
),
secondAction = EventMessageAction(
title = resourceReference(R.string.common_cancel),
onClick = {},
),
)

View file

@ -71,6 +71,7 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
route = configuration,
childContext = childByContext(factoryContext),
onManageTokensDone = model::onManageTokensDone,
model = model,
)
},
)

View file

@ -15,6 +15,8 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.BiometryFeatureToggles
@ -22,9 +24,11 @@ import com.tangem.features.onboarding.v2.TitleProvider
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent.Mode
import com.tangem.features.onboarding.v2.entry.impl.analytics.OnboardingEntryEvent
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.NonCancellable
@ -44,6 +48,7 @@ internal class OnboardingEntryModel @Inject constructor(
private val askBiometryFeatureToggles: BiometryFeatureToggles,
private val analyticsEventHandler: AnalyticsEventHandler,
private val uiMessageSender: UiMessageSender,
private val userWalletsListManager: UserWalletsListManager,
) : Model() {
private val params = paramsContainer.require<OnboardingEntryComponent.Params>()
@ -57,6 +62,7 @@ internal class OnboardingEntryModel @Inject constructor(
}
val startRoute = routeByProductType(params.scanResponse)
val onboardingTwinModelCallbacks = OnboardingTwinModelCallbacks()
fun onManageTokensDone() {
navigateToFinalScreenFlow()
@ -67,28 +73,25 @@ internal class OnboardingEntryModel @Inject constructor(
}
private fun routeByProductType(scanResponse: ScanResponse): OnboardingRoute {
val multiWalletNavigationMode = when (params.multiWalletMode) {
OnboardingEntryComponent.MultiWalletMode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding
OnboardingEntryComponent.MultiWalletMode.AddBackup -> OnboardingMultiWalletComponent.Mode.AddBackup
}
return when (scanResponse.productType) {
ProductType.Wallet -> OnboardingRoute.MultiWallet(
scanResponse = scanResponse,
withSeedPhraseFlow = false,
titleProvider = titleProvider,
onDone = ::onMultiWalletOnboardingDone,
mode = multiWalletNavigationMode,
)
ProductType.Ring,
ProductType.Wallet,
ProductType.Wallet2,
-> OnboardingRoute.MultiWallet(
scanResponse = scanResponse,
withSeedPhraseFlow = true,
titleProvider = titleProvider,
onDone = ::onMultiWalletOnboardingDone,
mode = multiWalletNavigationMode,
)
ProductType.Ring,
-> {
val multiWalletNavigationMode = when (params.mode) {
Mode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding
Mode.AddBackupWallet1 -> OnboardingMultiWalletComponent.Mode.AddBackup
else -> error("Incorrect onboarding type")
}
OnboardingRoute.MultiWallet(
scanResponse = scanResponse,
withSeedPhraseFlow = scanResponse.productType != ProductType.Wallet,
titleProvider = titleProvider,
onDone = ::onMultiWalletOnboardingDone,
mode = multiWalletNavigationMode,
)
}
ProductType.Visa -> OnboardingRoute.Visa(
scanResponse = scanResponse,
titleProvider = titleProvider,
@ -99,17 +102,31 @@ internal class OnboardingEntryModel @Inject constructor(
titleProvider = titleProvider,
onDone = ::navigateToFinalScreenFlow,
)
ProductType.Twins -> {
val mode = when (params.mode) {
Mode.Onboarding -> OnboardingTwinComponent.Params.Mode.CreateWallet
Mode.WelcomeOnlyTwin -> OnboardingTwinComponent.Params.Mode.WelcomeOnly
Mode.RecreateWalletTwin -> OnboardingTwinComponent.Params.Mode.RecreateWallet
else -> error("Incorrect onboarding type")
}
OnboardingRoute.Twins(
scanResponse = scanResponse,
titleProvider = titleProvider,
mode = mode,
)
}
else -> error("Unsupported")
}
}
private fun onMultiWalletOnboardingDone(userWallet: UserWallet) {
when {
params.multiWalletMode == OnboardingEntryComponent.MultiWalletMode.AddBackup -> {
params.mode == Mode.AddBackupWallet1 -> {
stackNavigation.replaceAll(
OnboardingRoute.Done(
mode = OnboardingDoneComponent.Mode.WalletCreated,
onDone = ::navigateToWalletScreen,
onDone = ::exitComponentScreen,
),
)
}
@ -139,7 +156,7 @@ internal class OnboardingEntryModel @Inject constructor(
stackNavigation.replaceAll(
OnboardingRoute.Done(
mode = doneMode,
onDone = ::navigateToWalletScreen,
onDone = ::exitComponentScreen,
),
)
}
@ -148,7 +165,7 @@ internal class OnboardingEntryModel @Inject constructor(
stackNavigation.replaceAll(
OnboardingRoute.Done(
mode = doneMode,
onDone = ::navigateToWalletScreen,
onDone = ::exitComponentScreen,
),
)
}
@ -162,7 +179,7 @@ internal class OnboardingEntryModel @Inject constructor(
stackNavigation.replaceAll(
OnboardingRoute.Done(
mode = doneMode,
onDone = ::navigateToWalletScreen,
onDone = ::exitComponentScreen,
),
)
}
@ -172,23 +189,55 @@ internal class OnboardingEntryModel @Inject constructor(
stackNavigation.replaceAll(
OnboardingRoute.Done(
mode = doneMode,
onDone = ::navigateToWalletScreen,
onDone = ::exitComponentScreen,
),
)
}
}
private fun navigateToWalletScreen() {
private fun exitComponentScreen() {
if (askBiometryFeatureToggles.isAskForBiometryEnabled) {
router.replaceAll(AppRoute.Wallet)
} else {
modelScope.launch(NonCancellable) {
router.replaceAll(AppRoute.Wallet)
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
delay(timeMillis = 1_800)
router.push(AppRoute.SaveWallet)
if (userWalletsListManager.hasUserWallets) {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false }
if (isLocked) {
router.replaceAll(AppRoute.Welcome())
} else {
router.replaceAll(AppRoute.Wallet)
}
} else {
router.replaceAll(AppRoute.Home)
}
} else {
if (userWalletsListManager.hasUserWallets) {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false }
if (isLocked) {
router.replaceAll(AppRoute.Welcome())
} else {
modelScope.launch(NonCancellable) {
router.replaceAll(AppRoute.Wallet)
if (tangemSdkManager.checkCanUseBiometry() &&
settingsRepository.shouldShowSaveUserWalletScreen()
) {
delay(timeMillis = 1_800)
router.push(AppRoute.SaveWallet)
}
}
}
} else {
router.replaceAll(AppRoute.Home)
}
}
}
inner class OnboardingTwinModelCallbacks : OnboardingTwinComponent.ModelCallbacks {
override fun onDone() {
navigateToFinalScreenFlow()
}
override fun onBack() {
router.pop()
}
}
}

View file

@ -4,11 +4,14 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
import com.tangem.features.onboarding.v2.entry.impl.model.OnboardingEntryModel
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent
import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent
import javax.inject.Inject
@Suppress("LongParameterList")
internal class OnboardingChildFactory @Inject constructor(
private val multiWalletComponentFactory: OnboardingMultiWalletComponent.Factory,
private val onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory,
@ -16,9 +19,15 @@ internal class OnboardingChildFactory @Inject constructor(
private val onBoardingVisaComponentFactory: OnboardingVisaComponent.Factory,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
private val onboardingNoteComponentFactory: OnboardingNoteComponent.Factory,
private val onboardingTwinComponentFactory: OnboardingTwinComponent.Factory,
) {
fun createChild(route: OnboardingRoute, childContext: AppComponentContext, onManageTokensDone: () -> Unit): Any {
fun createChild(
route: OnboardingRoute,
childContext: AppComponentContext,
model: OnboardingEntryModel,
onManageTokensDone: () -> Unit,
): Any {
return when (route) {
is OnboardingRoute.MultiWallet -> multiWalletComponentFactory.create(
context = childContext,
@ -60,6 +69,15 @@ internal class OnboardingChildFactory @Inject constructor(
onDone = route.onDone,
),
)
is OnboardingRoute.Twins -> onboardingTwinComponentFactory.create(
context = childContext,
params = OnboardingTwinComponent.Params(
scanResponse = route.scanResponse,
modelCallbacks = model.onboardingTwinModelCallbacks,
titleProvider = route.titleProvider,
mode = route.mode,
),
)
is OnboardingRoute.AskBiometry -> askBiometryComponentFactory.create(
context = childContext,
params = AskBiometryComponent.Params(

View file

@ -7,6 +7,7 @@ import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.onboarding.v2.TitleProvider
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent
sealed class OnboardingRoute : Route {
@ -32,6 +33,12 @@ sealed class OnboardingRoute : Route {
val onDone: () -> Unit,
) : OnboardingRoute()
data class Twins(
val titleProvider: TitleProvider,
val scanResponse: ScanResponse,
val mode: OnboardingTwinComponent.Params.Mode,
) : OnboardingRoute()
data class ManageTokens(
val userWallet: UserWallet,
) : OnboardingRoute()

View file

@ -20,20 +20,4 @@ internal fun resetCardDialog(onConfirm: () -> Unit, dismiss: () -> Unit, onDismi
onDismissButtonClick()
dismiss()
},
)
internal fun interruptBackupDialog(onConfirm: () -> Unit, dismiss: () -> Unit) = OnboardingDialogUM(
title = resourceReference(R.string.onboarding_exit_alert_title),
message = resourceReference(R.string.onboarding_exit_alert_message),
confirmButtonText = resourceReference(R.string.common_ok),
dismissButtonText = resourceReference(R.string.common_cancel),
dismissWarningColor = true,
onDismiss = dismiss,
onConfirmClick = {
onConfirm()
dismiss()
},
onDismissButtonClick = {
dismiss()
},
)
)

View file

@ -5,14 +5,15 @@ 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.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.common.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.interruptBackupDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.FinalizeStage
import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM
import com.tangem.operations.backup.BackupService
@ -35,6 +36,7 @@ internal class OnboardingMultiWalletModel @Inject constructor(
private val backupServiceHolder: BackupServiceHolder,
private val onboardingRepository: OnboardingRepository,
private val getCardImageUseCase: GetCardImageUseCase,
private val uiMessageSender: UiMessageSender,
) : Model() {
private val params = paramsContainer.require<OnboardingMultiWalletComponent.Params>()
private val _uiState = MutableStateFlow(OnboardingMultiWalletUM())
@ -60,19 +62,16 @@ internal class OnboardingMultiWalletModel @Inject constructor(
}
fun onBack() {
_uiState.update { st ->
st.copy(
dialog = interruptBackupDialog(
onConfirm = {
modelScope.launch {
onboardingRepository.clearUnfinishedFinalizeOnboarding()
router.pop()
}
},
dismiss = { _uiState.update { it.copy(dialog = null) } },
),
)
}
uiMessageSender.send(
interruptBackupDialog(
onConfirm = {
modelScope.launch {
onboardingRepository.clearUnfinishedFinalizeOnboarding()
router.pop()
}
},
),
)
}
private fun subscribeToBackups() {

View file

@ -8,9 +8,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM
@ -23,23 +20,6 @@ internal fun OnboardingMultiWallet(
modifier: Modifier = Modifier,
childContent: @Composable (Modifier) -> Unit = {},
) {
if (state.dialog != null) {
BasicDialog(
title = state.dialog.title.resolveReference(),
message = state.dialog.message.resolveReference(),
confirmButton = DialogButtonUM(
title = state.dialog.confirmButtonText.resolveReference(),
onClick = state.dialog.onConfirmClick,
),
dismissButton = DialogButtonUM(
title = state.dialog.dismissButtonText.resolveReference(),
warning = state.dialog.dismissWarningColor,
onClick = state.dialog.onDismissButtonClick,
),
onDismissDialog = state.dialog.onDismiss,
)
}
Column(
modifier = modifier.fillMaxSize(),
) {

View file

@ -1,10 +1,7 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.ui.state
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
internal data class OnboardingMultiWalletUM(
val artwork1Url: String? = null,
val artwork2Url: String? = null,
val artwork3Url: String? = null,
val dialog: OnboardingDialogUM? = null,
)

View file

@ -8,18 +8,19 @@ import com.tangem.features.onboarding.v2.TitleProvider
interface OnboardingTwinComponent : ComposableContentComponent {
data class Params(
val titleProvider: TitleProvider,
val scanResponse: ScanResponse,
val modelCallbacks: ModelCallbacks,
val titleProvider: TitleProvider,
val mode: Mode,
) {
enum class Mode {
CreateWallet, RecreateWallet
WelcomeOnly, CreateWallet, RecreateWallet
}
}
interface ModelCallbacks {
fun onDone()
fun onBack()
}
interface Factory : ComponentFactory<Params, OnboardingTwinComponent>

View file

@ -1,9 +1,12 @@
package com.tangem.features.onboarding.v2.twin.impl
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent
import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel
import dagger.assisted.Assisted
@ -17,9 +20,15 @@ internal class DefaultOnboardingTwinComponent @AssistedInject constructor(
private val model: OnboardingTwinModel = getOrCreateModel(params)
init {
params.titleProvider.changeTitle(resourceReference(R.string.twins_recreate_toolbar))
}
@Composable
override fun Content(modifier: Modifier) {
TODO("Not yet implemented")
BackHandler { model.onBack() }
// TODO
}
@AssistedFactory

View file

@ -0,0 +1,34 @@
package com.tangem.features.onboarding.v2.twin.impl.model
import com.tangem.core.ui.extensions.TextReference
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 TwinProcessNotCompleted = DialogMessage(
message = resourceReference(R.string.onboarding_twin_exit_warning),
firstAction = EventMessageAction(
title = resourceReference(R.string.warning_button_ok),
onClick = {},
),
)
internal fun cardVerificationFailedDialog(
errorDescription: TextReference,
onRequestSupport: () -> Unit,
onCancelClick: () -> Unit = {},
): DialogMessage {
return DialogMessage(
message = errorDescription,
title = resourceReference(id = R.string.security_alert_title),
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(id = R.string.alert_button_request_support),
onClick = onRequestSupport,
)
},
secondActionBuilder = { cancelAction(onClick = onCancelClick) },
)
}

View file

@ -1,19 +1,434 @@
package com.tangem.features.onboarding.v2.twin.impl.model
import com.tangem.Message
import com.tangem.common.CompletionResult
import com.tangem.common.KeyPair
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
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.navigation.url.UrlOpener
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent.Params.Mode
import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM
import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList", "LargeClass")
@ModelScoped
internal class OnboardingTwinModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
private val userWalletsListManager: UserWalletsListManager,
private val analyticsEventHandler: AnalyticsEventHandler,
private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase,
private val tangemSdkManager: TangemSdkManager,
private val issuersConfigStorage: IssuersConfigStorage,
private val cardRepository: CardRepository,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase,
private val urlOpener: UrlOpener,
private val uiMessageSender: UiMessageSender,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
) : Model() {
val params = paramsContainer.require<OnboardingTwinComponent.Params>()
private val params = paramsContainer.require<OnboardingTwinComponent.Params>()
private val firstCardTwinNumber = params.scanResponse.card.getTwinCardNumber() ?: error("Not twin")
private val cryptoCurrencyStatusJobHolder = JobHolder()
// TODO
private val _uiState = MutableStateFlow(
when (params.mode) {
Mode.WelcomeOnly -> {
OnboardingTwinUM.Welcome(onContinueClick = ::saveWalletAndDone)
}
Mode.RecreateWallet -> {
OnboardingTwinUM.ResetWarning(
onAcceptClick = { toggle ->
update<OnboardingTwinUM.ResetWarning> { it.copy(acceptToggle = toggle) }
},
onContinueClick = {
navigateToFirstScan()
},
)
}
Mode.CreateWallet -> {
OnboardingTwinUM.Welcome(onContinueClick = ::navigateToFirstScan)
}
},
)
val uiState = _uiState.asStateFlow()
init {
if (_uiState.value is OnboardingTwinUM.Welcome) {
analyticsEventHandler.send(OnboardingEvent.Twins.ScreenOpened)
modelScope.launch {
saveTwinsOnboardingShownUseCase()
}
}
}
fun onBack() {
val state = _uiState.value
val twinningInProgress = state is OnboardingTwinUM.ScanCard &&
(state.step == OnboardingTwinUM.ScanCard.Step.Second || state.step == OnboardingTwinUM.ScanCard.Step.Third)
if (twinningInProgress) {
uiMessageSender.send(TwinProcessNotCompleted)
} else {
uiMessageSender.send(
interruptBackupDialog(
onConfirm = { params.modelCallbacks.onBack() },
),
)
}
}
private fun navigateToFirstScan() {
_uiState.value = OnboardingTwinUM.ScanCard(
artworkStep = when (firstCardTwinNumber) {
TwinCardNumber.First -> TwinWalletArtworkUM.Leapfrog.Step.FirstCard
TwinCardNumber.Second -> TwinWalletArtworkUM.Leapfrog.Step.SecondCard
},
step = OnboardingTwinUM.ScanCard.Step.First,
onScanClick = { createFirstWallet() },
)
}
private fun createFirstWallet() {
setLoading(true)
modelScope.launch {
val result = tangemSdkManager.createFirstTwinWallet(
cardId = params.scanResponse.card.cardId,
initialMessage = Message(
tangemSdkManager.getString(
R.string.twins_recreate_title_format,
wrappedList(firstCardTwinNumber.number),
),
),
)
when (result) {
is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error)
is CompletionResult.Success -> {
if (params.mode == Mode.CreateWallet) {
cardRepository.startCardActivation(result.data.cardId)
}
// remove wallet only after first step of retwin
if (params.mode == Mode.RecreateWallet) {
userWalletsListManager.delete(
listOfNotNull(UserWalletIdBuilder.scanResponse(params.scanResponse).build()),
)
}
analyticsEventHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully())
update<OnboardingTwinUM.ScanCard> {
it.copy(
isLoading = false,
artworkStep = it.artworkStep.next(),
step = OnboardingTwinUM.ScanCard.Step.Second,
onScanClick = {
createSecondWallet(firstPublicKey = result.data.wallet.publicKey.toHexString())
},
)
}
}
}
}
}
private fun createSecondWallet(firstPublicKey: String) {
setLoading(true)
modelScope.launch {
val secondCardNumber = firstCardTwinNumber.pairNumber().number
val result = tangemSdkManager.createSecondTwinWallet(
firstPublicKey = firstPublicKey,
firstCardId = params.scanResponse.card.cardId,
issuerKeys = getIssuerKeys(),
preparingMessage = Message(
tangemSdkManager.getString(R.string.twins_recreate_title_preparing),
),
creatingWalletMessage = Message(
tangemSdkManager.getString(R.string.twins_recreate_title_creating_wallet),
),
initialMessage = Message(
tangemSdkManager.getString(
R.string.twins_recreate_title_format,
wrappedList(secondCardNumber),
),
),
)
when (result) {
is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error)
is CompletionResult.Success -> {
if (params.mode == Mode.CreateWallet) {
cardRepository.startCardActivation(result.data.cardId)
}
update<OnboardingTwinUM.ScanCard> {
it.copy(
isLoading = false,
artworkStep = it.artworkStep.next(),
step = OnboardingTwinUM.ScanCard.Step.Third,
onScanClick = {
createThirdWallet(
secondCardPublicKey = result.data.wallet.publicKey,
)
},
)
}
}
}
}
}
private fun createThirdWallet(secondCardPublicKey: ByteArray) {
setLoading(true)
modelScope.launch {
val result = tangemSdkManager.finalizeTwin(
secondCardPublicKey = secondCardPublicKey,
issuerKeyPair = getIssuerKeys(),
cardId = params.scanResponse.card.cardId,
initialMessage = Message(
tangemSdkManager.getString(
R.string.twins_recreate_title_format,
wrappedList(firstCardTwinNumber.number),
),
),
)
when (result) {
is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error)
is CompletionResult.Success -> {
scanComplete(result.data)
}
}
}
}
private fun scanComplete(scanResponse: ScanResponse) {
analyticsEventHandler.send(OnboardingEvent.Twins.SetupFinished)
when (params.mode) {
Mode.WelcomeOnly -> saveWalletAndDone()
Mode.RecreateWallet -> params.modelCallbacks.onDone()
Mode.CreateWallet -> {
modelScope.launch {
setLoading(true)
val userWallet = userWalletBuilderFactory.create(scanResponse).build() ?: run {
Timber.e("User wallet not created")
setLoading(false)
return@launch
}
fetchCurrencyStatusUseCase(
userWalletId = userWallet.walletId,
refresh = true,
)
val cryptoCurrencyStatus =
getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId).firstOrNull()?.getOrNull()
?: run {
setLoading(false)
return@launch
}
launch {
getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId)
.collect {
it.onRight { status ->
applyCryptoCurrencyStatusToState(status)
}
}
}
_uiState.value = OnboardingTwinUM.TopUp(
onBuyCryptoClick = { onBuyCryptoClick(cryptoCurrencyStatus) },
onRefreshClick = { onRefreshBalanceClick(userWallet) },
onShowAddressClick = { onShowAddressClick(cryptoCurrencyStatus) },
isLoading = true,
)
}.saveIn(cryptoCurrencyStatusJobHolder)
}
}
}
private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) {
update<OnboardingTwinUM.TopUp> {
it.copy(
balance = (status.value as? CryptoCurrencyStatus.Loaded)?.amount
?.format { crypto(status.currency) } ?: "",
onBuyCryptoClick = { onBuyCryptoClick(status) },
onShowAddressClick = { onShowAddressClick(status) },
isLoading = false,
)
}
}
private fun onBuyCryptoClick(status: CryptoCurrencyStatus) {
modelScope.launch {
getLegacyTopUpUrlUseCase(status).onRight {
urlOpener.openUrl(it)
}
}
}
private fun onShowAddressClick(status: CryptoCurrencyStatus) {
val currency = status.currency
val networkAddress = status.value.networkAddress ?: return
update<OnboardingTwinUM.TopUp> {
it.copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {
update<OnboardingTwinUM.TopUp> {
it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty)
}
},
content = TokenReceiveBottomSheetConfig(
name = currency.name,
symbol = currency.symbol,
network = currency.network.name,
addresses = networkAddress.availableAddresses
.mapToAddressModels(currency)
.toImmutableList(),
showMemoDisclaimer =
currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
onCopyClick = {},
onShareClick = {},
),
),
)
}
}
private fun onRefreshBalanceClick(userWallet: UserWallet) {
update<OnboardingTwinUM.TopUp> {
it.copy(isLoading = true)
}
modelScope.launch {
fetchCurrencyStatusUseCase(
userWalletId = userWallet.walletId,
refresh = true,
)
}
}
private fun saveWalletAndDone() {
setLoading(true)
modelScope.launch {
val userWallet = userWalletBuilderFactory.create(params.scanResponse).build() ?: run {
Timber.e("User wallet not created")
setLoading(false)
return@launch
}
userWalletsListManager.save(userWallet, canOverride = true)
params.modelCallbacks.onDone()
}
}
private fun showCardVerificationFailedDialog(error: TangemError) {
setLoading(false)
if (error !is TangemSdkError.CardVerificationFailed) return
analyticsEventHandler.send(OnboardingEvent.OfflineAttestationFailed(AnalyticsParam.ScreensSources.Onboarding))
val resource = error.localizedDescriptionRes()
val resId = resource.resId ?: R.string.common_unknown_error
val resArgs = resource.args.map { it.value }
uiMessageSender.send(
cardVerificationFailedDialog(
errorDescription = resourceReference(id = resId, resArgs.toWrappedList()),
onRequestSupport = {
modelScope.launch {
sendFeedbackEmailUseCase(type = FeedbackEmailType.CardAttestationFailed)
}
},
),
)
}
private suspend fun getIssuerKeys(): KeyPair {
val issuer = issuersConfigStorage.getConfig()
.first { it.publicKey == params.scanResponse.card.issuer.publicKey.toHexString() }
return KeyPair(
publicKey = issuer.publicKey.hexToBytes(),
privateKey = issuer.privateKey.hexToBytes(),
)
}
private inline fun <reified T : OnboardingTwinUM> update(updateBlock: (T) -> OnboardingTwinUM) {
_uiState.update { st ->
(st as? T)?.let { updateBlock(it) } ?: st
}
}
private fun setLoading(flag: Boolean) {
_uiState.update { it.copySealed(isLoading = flag) }
}
private fun TwinWalletArtworkUM.Leapfrog.Step.next(): TwinWalletArtworkUM.Leapfrog.Step = when (this) {
TwinWalletArtworkUM.Leapfrog.Step.FirstCard -> TwinWalletArtworkUM.Leapfrog.Step.SecondCard
TwinWalletArtworkUM.Leapfrog.Step.SecondCard -> TwinWalletArtworkUM.Leapfrog.Step.FirstCard
}
}

View file

@ -5,22 +5,28 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM
@Suppress("UnusedPrivateMember")
@Composable
fun OnboardingTwin(modifier: Modifier = Modifier) {
Column {
internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Box {
// TODO
}
}
if (state is OnboardingTwinUM.TopUp) {
TokenReceiveBottomSheet(config = state.bottomSheetConfig)
}
}
@Preview
@Composable
private fun Preview() {
TangemThemePreview {
OnboardingTwin()
OnboardingTwin(OnboardingTwinUM.Welcome())
}
}

View file

@ -33,18 +33,19 @@ import com.tangem.features.onboarding.v2.impl.R
import kotlinx.coroutines.delay
import java.util.concurrent.TimeUnit
internal sealed class TwinWalletArtworkState {
data object Spread : TwinWalletArtworkState()
@Immutable
internal sealed class TwinWalletArtworkUM {
data object Spread : TwinWalletArtworkUM()
data class Leapfrog(
val step: Step,
) : TwinWalletArtworkState() {
) : TwinWalletArtworkUM() {
enum class Step {
FirstCard, SecondCard
}
}
data object TopUp : TwinWalletArtworkState()
data object TopUp : TwinWalletArtworkUM()
}
private data class CardsTransitionState(
@ -65,7 +66,7 @@ private data class WalletCardTransitionState(
@Suppress("LongMethod")
@Composable
internal fun TwinWalletArtworks(
state: TwinWalletArtworkState,
state: TwinWalletArtworkUM,
balance: String,
isRefreshing: Boolean,
onRefreshBalanceClick: () -> Unit,
@ -109,7 +110,7 @@ internal fun TwinWalletArtworks(
}
AnimatedVisibility(
visible = state == TwinWalletArtworkState.TopUp,
visible = state == TwinWalletArtworkUM.TopUp,
enter = fadeIn(),
exit = fadeOut(),
) {
@ -134,7 +135,7 @@ internal fun TwinWalletArtworks(
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = state == TwinWalletArtworkState.TopUp,
visible = state == TwinWalletArtworkUM.TopUp,
enter = fadeIn(),
exit = fadeOut(),
) {
@ -151,7 +152,7 @@ internal fun TwinWalletArtworks(
)
SpacerH8()
Text(
text = balance.orEmpty(),
text = balance,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
@ -162,7 +163,7 @@ internal fun TwinWalletArtworks(
AnimatedVisibility(
modifier = Modifier.align(Alignment.BottomCenter),
visible = state == TwinWalletArtworkState.TopUp,
visible = state == TwinWalletArtworkUM.TopUp,
enter = fadeIn(),
exit = fadeOut(),
) {
@ -234,12 +235,12 @@ private fun AnimatedTwinCards(
}
@Suppress("MagicNumber", "LongMethod", "UnnecessaryParentheses")
private fun TwinWalletArtworkState.toTransitionSetState(
private fun TwinWalletArtworkUM.toTransitionSetState(
maxWidthDp: Float,
maxHeightDp: Float,
density: Float,
): List<CardsTransitionState> = when (this) {
TwinWalletArtworkState.Spread -> {
TwinWalletArtworkUM.Spread -> {
val scale = 0.8f
listOf(
CardsTransitionState(
@ -262,7 +263,7 @@ private fun TwinWalletArtworkState.toTransitionSetState(
),
)
}
is TwinWalletArtworkState.Leapfrog -> {
is TwinWalletArtworkUM.Leapfrog -> {
val translationY = -(maxHeightDp / 2) * density
val secondCardPlace = WalletCardTransitionState().copy(
xTranslation = 0f,
@ -272,7 +273,7 @@ private fun TwinWalletArtworkState.toTransitionSetState(
yScale = 0.83f,
)
if (step == TwinWalletArtworkState.Leapfrog.Step.FirstCard) {
if (step == TwinWalletArtworkUM.Leapfrog.Step.FirstCard) {
listOf(
CardsTransitionState(
walletCard1 = WalletCardTransitionState(
@ -306,7 +307,7 @@ private fun TwinWalletArtworkState.toTransitionSetState(
)
}
}
TwinWalletArtworkState.TopUp -> {
TwinWalletArtworkUM.TopUp -> {
val scale = 0.4f
val yTranslation = -maxHeightDp * density - 24 * density
listOf(
@ -339,7 +340,7 @@ private fun Preview() {
.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
var state: TwinWalletArtworkState by remember { mutableStateOf(TwinWalletArtworkState.TopUp) }
var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.TopUp) }
TwinWalletArtworks(
state = state,
@ -359,12 +360,12 @@ private fun Preview() {
.align(Alignment.TopStart),
onClick = {
val list = listOf(
TwinWalletArtworkState.Spread,
TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.FirstCard),
TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.SecondCard),
TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.FirstCard),
TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.SecondCard),
TwinWalletArtworkState.TopUp,
TwinWalletArtworkUM.Spread,
TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard),
TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard),
TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard),
TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard),
TwinWalletArtworkUM.TopUp,
)
state = list[index % list.size]

View file

@ -0,0 +1,65 @@
package com.tangem.features.onboarding.v2.twin.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM
@Immutable
internal sealed class OnboardingTwinUM {
abstract val stepIndex: Int
abstract val isLoading: Boolean
abstract val artwork: TwinWalletArtworkUM
data class Welcome(
override val isLoading: Boolean = false,
val onContinueClick: () -> Unit = {},
) : OnboardingTwinUM() {
override val stepIndex = 0
override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread
}
data class ResetWarning(
val acceptToggle: Boolean = false,
val onAcceptClick: (Boolean) -> Unit = {},
val onContinueClick: () -> Unit = {},
) : OnboardingTwinUM() {
override val stepIndex: Int = 0
override val isLoading: Boolean = false
override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread
}
data class ScanCard(
override val isLoading: Boolean = false,
val artworkStep: TwinWalletArtworkUM.Leapfrog.Step,
val step: Step = Step.First,
val onScanClick: () -> Unit = {},
) : OnboardingTwinUM() {
enum class Step {
First, Second, Third
}
override val stepIndex: Int = 1
override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Leapfrog(artworkStep)
}
data class TopUp(
override val isLoading: Boolean = false,
val balance: String = "",
val bottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty,
val onBuyCryptoClick: () -> Unit = {},
val onShowAddressClick: () -> Unit = {},
val onRefreshClick: () -> Unit = {},
) : OnboardingTwinUM() {
override val stepIndex: Int = 2
override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.TopUp
}
fun copySealed(isLoading: Boolean = this.isLoading): OnboardingTwinUM = when (this) {
is Welcome -> copy(isLoading = isLoading)
is ResetWarning -> copy()
is ScanCard -> copy(isLoading = isLoading)
is TopUp -> copy(isLoading = isLoading)
}
}

View file

@ -164,7 +164,7 @@ internal class WalletSettingsModel @Inject constructor(
router.push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.AddBackup,
mode = AppRoute.Onboarding.Mode.AddBackupWallet1,
),
)
} else {

View file

@ -54,7 +54,7 @@ internal class DefaultWalletRouter @Inject constructor(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = if (continueBackup) {
AppRoute.Onboarding.Mode.AddBackup
AppRoute.Onboarding.Mode.AddBackupWallet1
} else {
AppRoute.Onboarding.Mode.Onboarding
},