Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-19 15:55:14 +05:00
parent 1c192d52b2
commit c6aa63a44f
13 changed files with 465 additions and 3 deletions

View file

@ -0,0 +1,14 @@
package com.tangem.features.hotwallet
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
interface CreateWalletBackupComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
)
interface Factory : ComponentFactory<Params, CreateWalletBackupComponent>
}

View file

@ -0,0 +1,97 @@
package com.tangem.features.hotwallet.createwalletbackup
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.push
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.domain.models.wallet.UserWalletId
import com.tangem.features.hotwallet.CreateWalletBackupComponent
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute
import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent
import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
@ModelScoped
internal class CreateWalletBackupModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
) : Model() {
val params = paramsContainer.require<CreateWalletBackupComponent.Params>()
val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback()
val manualBackupStartModelCallbacks = ManualBackupStartModelCallbacks()
val manualBackupPhraseModelCallbacks = ManualBackupPhraseModelCallbacks()
val manualBackupCheckModelCallbacks = ManualBackupCheckModelCallbacks()
val manualBackupCompletedModelCallbacks = ManualBackupCompletedModelCallbacks()
val stackNavigation = StackNavigation<CreateWalletBackupRoute>()
val startRoute = CreateWalletBackupRoute.RecoveryPhraseStart
val currentRoute: MutableStateFlow<CreateWalletBackupRoute> = MutableStateFlow(startRoute)
fun onBack() {
when (currentRoute.value) {
is CreateWalletBackupRoute.RecoveryPhraseStart -> router.pop()
is CreateWalletBackupRoute.RecoveryPhrase -> stackNavigation.pop()
is CreateWalletBackupRoute.ConfirmBackup -> stackNavigation.pop()
is CreateWalletBackupRoute.BackupCompleted -> router.pop()
}
}
fun onManualBackupStarted() {
stackNavigation.push(CreateWalletBackupRoute.RecoveryPhrase)
}
fun onManualBackupPhraseShown() {
stackNavigation.push(CreateWalletBackupRoute.ConfirmBackup)
}
fun onManualBackupChecked() {
stackNavigation.push(CreateWalletBackupRoute.BackupCompleted)
}
fun onManualBackupCompleted() {
router.pop()
}
inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback {
override fun onBackClick() {
onBack()
}
override fun onSkipClick() = Unit
}
inner class ManualBackupStartModelCallbacks : ManualBackupStartComponent.ModelCallbacks {
override fun onContinueClick() {
onManualBackupStarted()
}
}
inner class ManualBackupPhraseModelCallbacks : ManualBackupPhraseComponent.ModelCallbacks {
override fun onContinueClick() {
onManualBackupPhraseShown()
}
}
inner class ManualBackupCheckModelCallbacks : ManualBackupCheckComponent.ModelCallbacks {
override fun onCompleteClick() {
onManualBackupChecked()
}
}
inner class ManualBackupCompletedModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks {
override fun onContinueClick(userWalletId: UserWalletId) {
onManualBackupCompleted()
}
}
}

View file

@ -0,0 +1,56 @@
package com.tangem.features.hotwallet.createwalletbackup
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute
import com.tangem.features.hotwallet.impl.R
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import javax.inject.Inject
internal class CreateWalletBackupStepperStateManager @Inject constructor() {
fun getStepperState(route: CreateWalletBackupRoute): HotWalletStepperComponent.StepperUM? {
return when (route) {
is CreateWalletBackupRoute.RecoveryPhraseStart -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_START,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_backup),
showBackButton = true,
showSkipButton = false,
showFeedbackButton = true,
)
is CreateWalletBackupRoute.RecoveryPhrase -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_PHRASE,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_backup),
showBackButton = true,
showSkipButton = false,
showFeedbackButton = true,
)
is CreateWalletBackupRoute.ConfirmBackup -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_CONFIRM,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_backup),
showBackButton = true,
showSkipButton = false,
showFeedbackButton = true,
)
is CreateWalletBackupRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_COMPLETED,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_done),
showBackButton = false,
showSkipButton = false,
showFeedbackButton = false,
)
}
}
companion object {
private const val STEPS_COUNT = 4
private const val STEP_START = 1
private const val STEP_PHRASE = 2
private const val STEP_CONFIRM = 3
private const val STEP_COMPLETED = 4
}
}

View file

@ -0,0 +1,92 @@
package com.tangem.features.hotwallet.createwalletbackup
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.value.ObserveLifecycleMode
import com.arkivanov.decompose.value.subscribe
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.hotwallet.CreateWalletBackupComponent
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupChildFactory
import com.tangem.features.hotwallet.createwalletbackup.ui.CreateWalletBackupContent
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.launch
internal class DefaultCreateWalletBackupComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: CreateWalletBackupComponent.Params,
private val stepperStateManager: CreateWalletBackupStepperStateManager,
createWalletBackupChildFactory: CreateWalletBackupChildFactory,
stepperComponentFactory: DefaultHotWalletStepperComponent.Factory,
) : CreateWalletBackupComponent, AppComponentContext by appComponentContext {
private val model: CreateWalletBackupModel = getOrCreateModel(params)
private val innerStack = childStack(
key = "createWalletBackupInnerStack",
source = model.stackNavigation,
serializer = null,
initialConfiguration = model.startRoute,
handleBackButton = true,
childFactory = { configuration, factoryContext ->
createWalletBackupChildFactory.createChild(
route = configuration,
childContext = childByContext(factoryContext),
model = model,
)
},
)
private val stepperComponent = stepperComponentFactory.create(
context = this,
params = HotWalletStepperComponent.Params(
initState = HotWalletStepperComponent.StepperUM.initialState(),
callback = model.hotWalletStepperComponentModelCallback,
),
)
init {
innerStack.subscribe(
lifecycle = lifecycle,
mode = ObserveLifecycleMode.CREATE_DESTROY,
) { stack ->
componentScope.launch {
model.currentRoute.emit(stack.active.configuration)
}
}
}
@Composable
override fun Content(modifier: Modifier) {
val stackState by innerStack.subscribeAsState()
val currentRoute = stackState.active.configuration
BackHandler(onBack = model::onBack)
val stepperState = stepperStateManager.getStepperState(currentRoute)
stepperState?.let { stepperComponent.updateState(it) }
CreateWalletBackupContent(
stackState = stackState,
stepperComponent = stepperComponent.takeIf { stepperState != null },
modifier = modifier,
)
}
@AssistedFactory
interface Factory : CreateWalletBackupComponent.Factory {
override fun create(
context: AppComponentContext,
params: CreateWalletBackupComponent.Params,
): DefaultCreateWalletBackupComponent
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.features.hotwallet.createwalletbackup.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.CreateWalletBackupComponent
import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel
import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupStepperStateManager
import com.tangem.features.hotwallet.createwalletbackup.DefaultCreateWalletBackupComponent
import dagger.Binds
import dagger.Module
import dagger.Provides
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 CreateWalletBackupModuleBinds {
@Binds
@Singleton
fun bindCreateWalletBackupComponentFactory(
impl: DefaultCreateWalletBackupComponent.Factory,
): CreateWalletBackupComponent.Factory
@Binds
@IntoMap
@ClassKey(CreateWalletBackupModel::class)
fun bindCreateWalletBackupModel(model: CreateWalletBackupModel): Model
}
@Module
@InstallIn(SingletonComponent::class)
internal object CreateWalletBackupModule {
@Provides
@Singleton
fun provideCreateWalletBackupStepperStateManager(): CreateWalletBackupStepperStateManager {
return CreateWalletBackupStepperStateManager()
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.features.hotwallet.createwalletbackup.routing
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel
import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent
import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent
import javax.inject.Inject
internal class CreateWalletBackupChildFactory @Inject constructor() {
fun createChild(
route: CreateWalletBackupRoute,
childContext: AppComponentContext,
model: CreateWalletBackupModel,
): ComposableContentComponent = when (route) {
CreateWalletBackupRoute.RecoveryPhraseStart -> ManualBackupStartComponent(
context = childContext,
params = ManualBackupStartComponent.Params(
callbacks = model.manualBackupStartModelCallbacks,
),
)
CreateWalletBackupRoute.RecoveryPhrase -> ManualBackupPhraseComponent(
context = childContext,
params = ManualBackupPhraseComponent.Params(
userWalletId = model.params.userWalletId,
callbacks = model.manualBackupPhraseModelCallbacks,
),
)
CreateWalletBackupRoute.ConfirmBackup -> ManualBackupCheckComponent(
context = childContext,
params = ManualBackupCheckComponent.Params(
userWalletId = model.params.userWalletId,
callbacks = model.manualBackupCheckModelCallbacks,
),
)
CreateWalletBackupRoute.BackupCompleted -> ManualBackupCompletedComponent(
context = childContext,
params = ManualBackupCompletedComponent.Params(
userWalletId = model.params.userWalletId,
callbacks = model.manualBackupCompletedModelCallbacks,
),
)
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.hotwallet.createwalletbackup.routing
import kotlinx.serialization.Serializable
@Serializable
internal sealed interface CreateWalletBackupRoute {
@Serializable
data object RecoveryPhraseStart : CreateWalletBackupRoute
@Serializable
data object RecoveryPhrase : CreateWalletBackupRoute
@Serializable
data object ConfirmBackup : CreateWalletBackupRoute
@Serializable
data object BackupCompleted : CreateWalletBackupRoute
}

View file

@ -0,0 +1,42 @@
package com.tangem.features.hotwallet.createwalletbackup.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
@Composable
internal fun CreateWalletBackupContent(
stackState: ChildStack<CreateWalletBackupRoute, ComposableContentComponent>,
stepperComponent: HotWalletStepperComponent?,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.background(color = TangemTheme.colors.background.primary)
.fillMaxSize()
.imePadding()
.systemBarsPadding(),
) {
stepperComponent?.Content(Modifier)
Children(
stack = stackState,
animation = stackAnimation(slide()),
modifier = Modifier.fillMaxSize(),
) {
it.instance.Content(Modifier.fillMaxSize())
}
}
}

View file

@ -18,6 +18,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.common.routing.AppRoute
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -66,8 +67,8 @@ internal class WalletBackupModel @Inject constructor(
secondaryButton {
text = resourceReference(R.string.hw_backup_need_action)
onClick {
router.push(AppRoute.CreateWalletBackup(params.userWalletId))
closeBs()
// TODO [REDACTED_TASK_KEY]
}
}
}

View file

@ -9,4 +9,5 @@ internal data class WalletSettingsUM(
val items: PersistentList<WalletSettingsItemUM>,
val requestPushNotificationsPermission: Boolean = false,
val onPushNotificationPermissionGranted: (Boolean) -> Unit,
val isWalletBackedUp: Boolean = true,
)

View file

@ -15,10 +15,16 @@ 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.navigation.settings.SettingsManager
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import com.tangem.core.ui.components.bottomsheets.message.icon
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
import com.tangem.core.ui.components.bottomsheets.message.onClick
import com.tangem.core.ui.components.bottomsheets.message.secondaryButton
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.scan.CardDTO
@ -86,9 +92,29 @@ internal class WalletSettingsModel @Inject constructor(
items = persistentListOf(),
requestPushNotificationsPermission = false,
onPushNotificationPermissionGranted = ::onPushNotificationPermissionGranted,
isWalletBackedUp = true,
),
)
private val makeBackupAtFirstAlertBS
get() = bottomSheetMessage {
infoBlock {
icon(R.drawable.ic_passcode_lock_32) {
type = MessageBottomSheetUMV2.Icon.Type.Accent
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
}
title = resourceReference(R.string.hw_backup_need_title)
body = resourceReference(R.string.hw_backup_need_description)
}
secondaryButton {
text = resourceReference(R.string.hw_backup_need_action)
onClick {
router.push(AppRoute.CreateWalletBackup(params.userWalletId))
closeBs()
}
}
}
init {
combine(
getWalletUseCase.invokeFlow(params.userWalletId).distinctUntilChanged(),
@ -97,6 +123,10 @@ internal class WalletSettingsModel @Inject constructor(
) { maybeWallet, nftEnabled, notificationsEnabled ->
val wallet = maybeWallet.getOrNull() ?: return@combine
val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase()
val isWalletBackedUp = when (wallet) {
is UserWallet.Hot -> wallet.backedUp
is UserWallet.Cold -> true
}
val isNeedShowNotifications = notificationsToggles.isNotificationsEnabled &&
!getIsHuaweiDeviceWithoutGoogleServicesUseCase()
state.update { value ->
@ -110,6 +140,7 @@ internal class WalletSettingsModel @Inject constructor(
isNotificationsFeatureEnabled = isNeedShowNotifications,
isNotificationsPermissionGranted = isNotificationsPermissionGranted(),
),
isWalletBackedUp = isWalletBackedUp,
)
}
}
@ -330,6 +361,10 @@ internal class WalletSettingsModel @Inject constructor(
}
private fun onAccessCodeClick() {
router.push(AppRoute.UpdateAccessCode(params.userWalletId))
if (!state.value.isWalletBackedUp) {
messageSender.send(makeBackupAtFirstAlertBS)
} else {
router.push(AppRoute.UpdateAccessCode(params.userWalletId))
}
}
}