Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-28 13:57:47 +05:00
parent c89a95265d
commit c761f40891
13 changed files with 437 additions and 34 deletions

View file

@ -1,8 +1,9 @@
package com.tangem.features.hotwallet.addexistingwallet.root
package com.tangem.features.hotwallet.addexistingwallet.entry
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.push
import com.arkivanov.decompose.router.stack.replaceAll
import com.arkivanov.decompose.router.stack.replaceCurrent
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
@ -10,7 +11,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
import com.tangem.features.hotwallet.accesscode.confirm.ConfirmAccessCodeComponent
@ -43,14 +44,31 @@ internal class AddExistingWalletModel @Inject constructor(
when (currentRoute) {
is AddExistingWalletRoute.Import -> stackNavigation.pop()
is AddExistingWalletRoute.BackupCompleted -> Unit
is AddExistingWalletRoute.SetAccessCode -> Unit
is AddExistingWalletRoute.ConfirmAccessCode -> stackNavigation.pop()
is AddExistingWalletRoute.SetAccessCode -> stackNavigation.pop()
is AddExistingWalletRoute.PushNotifications -> Unit
is AddExistingWalletRoute.SetupFinished -> Unit
is AddExistingWalletRoute.Start -> Unit
}
}
fun onSkipAccessCode() {
navigateToPushNotificationsOrNext()
}
private fun navigateToPushNotificationsOrNext() {
modelScope.launch {
val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION)
if (shouldRequestPush) {
// is yet blocked by [REDACTED_TASK_KEY]
// stackNavigation.replaceAll(AddExistingWalletRoute.PushNotifications)
stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished)
} else {
stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished)
}
}
}
inner class AddExistingWalletStartModelCallbacks : AddExistingWalletStartComponent.ModelCallbacks {
override fun onBackClick() {
router.pop()
@ -69,7 +87,7 @@ internal class AddExistingWalletModel @Inject constructor(
inner class ManualBackupCompletedComponentModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks {
override fun onContinueClick() {
stackNavigation.push(AddExistingWalletRoute.SetAccessCode)
stackNavigation.replaceCurrent(AddExistingWalletRoute.SetAccessCode)
}
}
@ -81,16 +99,7 @@ internal class AddExistingWalletModel @Inject constructor(
inner class ConfirmAccessCodeModelCallbacks : ConfirmAccessCodeComponent.ModelCallbacks {
override fun onAccessCodeConfirmed() {
modelScope.launch {
val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION)
if (shouldRequestPush) {
// is yet blocked by [REDACTED_TASK_KEY]
// stackNavigation.replaceCurrent(AddExistingWalletRoute.PushNotifications)
stackNavigation.replaceCurrent(AddExistingWalletRoute.SetupFinished)
} else {
stackNavigation.replaceCurrent(AddExistingWalletRoute.SetupFinished)
}
}
navigateToPushNotificationsOrNext()
}
}

View file

@ -0,0 +1,79 @@
package com.tangem.features.hotwallet.addexistingwallet.entry
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute
import com.tangem.features.hotwallet.impl.R
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
internal class AddExistingWalletStepperStateManager {
fun getStepperState(route: AddExistingWalletRoute): HotWalletStepperComponent.StepperUM? {
return when (route) {
is AddExistingWalletRoute.Start -> null
is AddExistingWalletRoute.Import -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_IMPORT,
steps = STEPS_COUNT,
title = resourceReference(R.string.wallet_import_seed_navtitle),
showBackButton = true,
showSkipButton = false,
showFeedbackButton = true,
)
is AddExistingWalletRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_BACKUP,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_backup),
showBackButton = false,
showSkipButton = false,
showFeedbackButton = false,
)
is AddExistingWalletRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_ACCESS_CODE,
steps = STEPS_COUNT,
title = resourceReference(R.string.access_code_navtitle),
showBackButton = false,
showSkipButton = true,
showFeedbackButton = false,
)
is AddExistingWalletRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_ACCESS_CODE,
steps = STEPS_COUNT,
title = resourceReference(R.string.access_code_navtitle),
showBackButton = true,
showSkipButton = true,
showFeedbackButton = false,
)
is AddExistingWalletRoute.PushNotifications -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_NOTIFICATIONS,
steps = STEPS_COUNT,
title = resourceReference(R.string.onboarding_title_notifications),
showBackButton = false,
showSkipButton = false,
showFeedbackButton = false,
)
is AddExistingWalletRoute.SetupFinished -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_DONE,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_done),
showBackButton = false,
showSkipButton = false,
showFeedbackButton = false,
)
}
}
companion object {
private const val STEPS_COUNT = 5
private const val STEP_IMPORT = 1
private const val STEP_BACKUP = 2
private const val STEP_ACCESS_CODE = 3
private const val STEP_NOTIFICATIONS = 4
private const val STEP_DONE = 5
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.hotwallet.addexistingwallet.root
package com.tangem.features.hotwallet.addexistingwallet.entry
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
@ -6,14 +6,16 @@ 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.router.stack.pop
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.hotwallet.AddExistingWalletComponent
import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletChildFactory
import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute
import com.tangem.features.hotwallet.addexistingwallet.root.ui.AddExistingWalletContent
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletChildFactory
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute
import com.tangem.features.hotwallet.addexistingwallet.entry.ui.AddExistingWalletContent
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
@ -21,7 +23,9 @@ import dagger.assisted.AssistedInject
internal class DefaultAddExistingWalletComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: Unit,
private val stepperStateManager: AddExistingWalletStepperStateManager,
addExistingWalletChildFactory: AddExistingWalletChildFactory,
stepperComponentFactory: DefaultHotWalletStepperComponent.Factory,
) : AddExistingWalletComponent, AppComponentContext by appComponentContext {
private val model: AddExistingWalletModel = getOrCreateModel(params)
@ -43,13 +47,42 @@ internal class DefaultAddExistingWalletComponent @AssistedInject constructor(
},
)
private val stepperComponent = stepperComponentFactory.create(
context = this,
params = HotWalletStepperComponent.Params(
initState = HotWalletStepperComponent.StepperUM(
currentStep = 0,
steps = 0,
title = TextReference.EMPTY,
showBackButton = false,
showSkipButton = false,
showFeedbackButton = false,
),
callback = object : HotWalletStepperComponent.ModelCallback {
override fun onBackClick() {
onChildBack()
}
override fun onSkipClick() {
model.onSkipAccessCode()
}
},
),
)
@Composable
override fun Content(modifier: Modifier) {
val stackState by innerStack.subscribeAsState()
val currentRoute = stackState.active.configuration
BackHandler(onBack = ::onChildBack)
val stepperState = stepperStateManager.getStepperState(currentRoute)
stepperState?.let { stepperComponent.updateState(it) }
AddExistingWalletContent(
stackState = stackState,
stepperComponent = stepperComponent.takeIf { stepperState != null },
)
}

View file

@ -1,11 +1,16 @@
package com.tangem.features.hotwallet.addexistingwallet.root.di
package com.tangem.features.hotwallet.addexistingwallet.entry.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.AddExistingWalletComponent
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
import com.tangem.features.hotwallet.addexistingwallet.root.DefaultAddExistingWalletComponent
import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletModel
import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletStepperStateManager
import com.tangem.features.hotwallet.addexistingwallet.entry.DefaultAddExistingWalletComponent
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent
import com.tangem.features.hotwallet.stepper.impl.HotWalletStepperModel
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
@ -14,7 +19,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AddExistingWalletModule {
internal interface AddExistingWalletModuleBinds {
@Binds
@Singleton
@ -26,4 +31,23 @@ internal interface AddExistingWalletModule {
@IntoMap
@ClassKey(AddExistingWalletModel::class)
fun bindAddExistingWalletModel(model: AddExistingWalletModel): Model
@Binds
fun bindFactory(impl: DefaultHotWalletStepperComponent.Factory): HotWalletStepperComponent.Factory
@Binds
@IntoMap
@ClassKey(HotWalletStepperModel::class)
fun bindHotWalletStepperModel(model: HotWalletStepperModel): Model
}
@Module
@InstallIn(SingletonComponent::class)
internal object AddExistingWalletModule {
@Provides
@Singleton
fun provideAddExistingWalletStepperStateManager(): AddExistingWalletStepperStateManager {
return AddExistingWalletStepperStateManager()
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.hotwallet.addexistingwallet.root.entity
package com.tangem.features.hotwallet.addexistingwallet.entry.entity
internal data class AddExistingWalletUM(
val onBackClick: () -> Unit,

View file

@ -1,10 +1,10 @@
package com.tangem.features.hotwallet.addexistingwallet.root.routing
package com.tangem.features.hotwallet.addexistingwallet.entry.routing
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.accesscode.set.SetAccessCodeComponent
import com.tangem.features.hotwallet.accesscode.confirm.ConfirmAccessCodeComponent
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletModel
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent

View file

@ -1,4 +1,4 @@
package com.tangem.features.hotwallet.addexistingwallet.root.routing
package com.tangem.features.hotwallet.addexistingwallet.entry.routing
import com.tangem.core.decompose.navigation.Route
import kotlinx.serialization.Serializable

View file

@ -1,6 +1,7 @@
package com.tangem.features.hotwallet.addexistingwallet.root.ui
package com.tangem.features.hotwallet.addexistingwallet.entry.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
@ -12,19 +13,29 @@ 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.addexistingwallet.root.routing.AddExistingWalletRoute
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
@Composable
internal fun AddExistingWalletContent(stackState: ChildStack<AddExistingWalletRoute, ComposableContentComponent>) {
Children(
stack = stackState,
animation = stackAnimation(slide()),
internal fun AddExistingWalletContent(
stackState: ChildStack<AddExistingWalletRoute, ComposableContentComponent>,
stepperComponent: HotWalletStepperComponent?,
) {
Column(
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.fillMaxSize()
.imePadding()
.systemBarsPadding(),
) {
it.instance.Content(Modifier.fillMaxSize())
stepperComponent?.Content(Modifier)
Children(
stack = stackState,
animation = stackAnimation(slide()),
modifier = Modifier.fillMaxSize(),
) {
it.instance.Content(Modifier.fillMaxSize())
}
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.hotwallet.stepper.api
import androidx.annotation.IntRange
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
import kotlinx.coroutines.flow.StateFlow
interface HotWalletStepperComponent : ComposableContentComponent {
data class StepperUM(
@IntRange(from = 0) val currentStep: Int,
@IntRange(from = 0) val steps: Int,
val title: TextReference,
val showBackButton: Boolean,
val showSkipButton: Boolean,
val showFeedbackButton: Boolean,
)
interface ModelCallback {
fun onBackClick()
fun onSkipClick()
}
class Params(
val initState: StepperUM,
val callback: ModelCallback,
)
val state: StateFlow<StepperUM>
interface Factory : ComponentFactory<Params, HotWalletStepperComponent>
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.hotwallet.stepper.di
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal interface HotWalletStepperModule {
@Binds
fun bindHotWalletStepperComponentFactory(
impl: DefaultHotWalletStepperComponent.Factory,
): HotWalletStepperComponent.Factory
}

View file

@ -0,0 +1,48 @@
package com.tangem.features.hotwallet.stepper.impl
import androidx.compose.runtime.Composable
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.model.getOrCreateModel
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import com.tangem.features.hotwallet.stepper.impl.ui.HotWalletStepper
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultHotWalletStepperComponent @AssistedInject constructor(
@Assisted val context: AppComponentContext,
@Assisted val params: HotWalletStepperComponent.Params,
) : HotWalletStepperComponent, AppComponentContext by context {
private val model: HotWalletStepperModel = getOrCreateModel(params)
override val state = model.uiState
fun updateState(newState: HotWalletStepperComponent.StepperUM) {
model.updateState(newState)
}
@Composable
override fun Content(modifier: Modifier) {
val uiState by state.collectAsStateWithLifecycle()
HotWalletStepper(
state = uiState,
modifier = modifier,
onBackClick = model::onBackClick,
onSkipClick = model::onSkipClick,
onFeedbackClick = model::onFeedbackClick,
)
}
@AssistedFactory
interface Factory : HotWalletStepperComponent.Factory {
override fun create(
context: AppComponentContext,
params: HotWalletStepperComponent.Params,
): DefaultHotWalletStepperComponent
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.features.hotwallet.stepper.impl
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@Stable
@ModelScoped
internal class HotWalletStepperModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
val params = paramsContainer.require<HotWalletStepperComponent.Params>()
val uiState: StateFlow<HotWalletStepperComponent.StepperUM>
field = MutableStateFlow(params.initState)
fun updateState(newState: HotWalletStepperComponent.StepperUM) {
uiState.value = newState
}
fun onBackClick() {
params.callback.onBackClick()
}
fun onSkipClick() {
// TODO send analytics
params.callback.onSkipClick()
}
fun onFeedbackClick() {
// TODO send analytics
// openFeedback()
}
}

View file

@ -0,0 +1,106 @@
package com.tangem.features.hotwallet.stepper.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemAnimations
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.impl.R
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
@Composable
internal fun HotWalletStepper(
state: HotWalletStepperComponent.StepperUM,
onBackClick: () -> Unit,
onSkipClick: () -> Unit,
onFeedbackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val fraction = state.currentStep.toFloat() / state.steps.coerceAtLeast(1)
val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState(targetFraction = fraction)
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
TangemTopAppBar(
startButton = if (state.showBackButton) {
TopAppBarButtonUM.Back { onBackClick() }
} else {
null
},
endButton = when {
state.showSkipButton -> TopAppBarButtonUM.Text(
text = resourceReference(R.string.common_skip),
onClicked = { onSkipClick() },
)
state.showFeedbackButton -> TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_chat_24,
onClicked = { onFeedbackClick() },
)
else -> null
},
title = state.title,
containerColor = TangemTheme.colors.background.primary,
modifier = modifier,
titleAlignment = Alignment.CenterHorizontally,
)
TangemLinearProgressIndicator(
modifier = Modifier
.padding(horizontal = 16.dp)
.height(4.dp)
.fillMaxWidth(),
progress = { animatedIndicatorFraction },
color = TangemTheme.colors.icon.primary1,
backgroundColor = TangemTheme.colors.icon.primary1.copy(alpha = 0.4f),
strokeCap = StrokeCap.Round,
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun HotWalletStepper_Preview() {
TangemThemePreview {
Box(
Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
) {
HotWalletStepper(
modifier = Modifier.align(Alignment.TopCenter),
state = HotWalletStepperComponent.StepperUM(
currentStep = 2,
steps = 3,
title = resourceReference(R.string.common_done),
showBackButton = true,
showSkipButton = false,
showFeedbackButton = true,
),
onBackClick = {},
onSkipClick = {},
onFeedbackClick = {},
)
}
}
}