Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-11 15:53:28 +05:00
parent 410253dc5a
commit 3c3f35aeb6
26 changed files with 589 additions and 63 deletions

View file

@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.entry
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
interface OnboardingEntryComponent : ComposableContentComponent {
@ -11,12 +12,13 @@ interface OnboardingEntryComponent : ComposableContentComponent {
val mode: Mode,
)
enum class Mode {
Onboarding,
AddBackupWallet1,
WelcomeOnlyTwin,
RecreateWalletTwin,
ContinueFinalize,
sealed class Mode {
data object Onboarding : Mode()
data object AddBackupWallet1 : Mode()
data object WelcomeOnlyTwin : Mode()
data object RecreateWalletTwin : Mode()
data object ContinueFinalize : Mode()
data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode()
}
interface Factory : ComponentFactory<Params, OnboardingEntryComponent>

View file

@ -53,6 +53,7 @@ dependencies {
implementation(projects.domain.transaction)
/** Tangem libraries */
implementation(tangemDeps.hot.core)
implementation(projects.libs.tangemSdkApi)
implementation(tangemDeps.card.core)
implementation(tangemDeps.card.android) {

View file

@ -73,16 +73,20 @@ internal class OnboardingEntryModel @Inject constructor(
uiMessageSender.send(CantLeaveBackupDialog)
}
@Suppress("CyclomaticComplexMethod")
private fun routeByProductType(scanResponse: ScanResponse): OnboardingRoute {
return when (scanResponse.productType) {
ProductType.Wallet,
ProductType.Wallet2,
ProductType.Ring,
-> {
val multiWalletNavigationMode = when (params.mode) {
Mode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding
Mode.AddBackupWallet1 -> OnboardingMultiWalletComponent.Mode.AddBackup
Mode.ContinueFinalize -> OnboardingMultiWalletComponent.Mode.ContinueFinalize
val multiWalletNavigationMode = when (val mode = params.mode) {
is Mode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding
is Mode.AddBackupWallet1 -> OnboardingMultiWalletComponent.Mode.AddBackup
is Mode.ContinueFinalize -> OnboardingMultiWalletComponent.Mode.ContinueFinalize
is Mode.UpgradeHotWallet -> OnboardingMultiWalletComponent.Mode.UpgradeHotWallet(
userWalletId = mode.userWalletId,
)
else -> error("Incorrect onboarding type")
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.onboarding.v2.TitleProvider
interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavigationHolder {
@ -17,10 +18,11 @@ interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavi
val onDone: (UserWallet.Cold) -> Unit,
)
enum class Mode {
Onboarding,
AddBackup,
ContinueFinalize,
sealed class Mode {
data object Onboarding : Mode()
data object AddBackup : Mode()
data object ContinueFinalize : Mode()
data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode()
}
interface Factory : ComponentFactory<Params, OnboardingMultiWalletComponent>

View file

@ -35,6 +35,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.Mul
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.MultiWalletFinalizeComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.MultiWalletScanPrimaryComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.MultiWalletSeedPhraseComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.MultiWalletUpgradeWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.Step.*
@ -57,6 +58,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
private val artworksState = instanceKeeper.getOrCreateSimple(key = "artworksState") {
MutableStateFlow(
when (model.state.value.currentStep) {
UpgradeWallet -> WalletArtworksState.Folded
CreateWallet -> WalletArtworksState.Folded
ChooseBackupOption -> WalletArtworksState.Fan
SeedPhrase -> WalletArtworksState.Folded
@ -142,6 +144,11 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
childContext: AppComponentContext,
): ComposableContentComponent {
return when (step) {
UpgradeWallet -> MultiWalletUpgradeWalletComponent(
context = childContext,
params = childParams,
onNextStep = ::handleNavigationEvent,
)
CreateWallet -> MultiWalletCreateWalletComponent(
context = childContext,
params = childParams,

View file

@ -265,6 +265,16 @@ internal class MultiWalletFinalizeModel @Inject constructor(
userWallet
}
is OnboardingMultiWalletComponent.Mode.UpgradeHotWallet -> {
saveWalletUseCase(
userWallet = userWalletCreated.copy(
scanResponse = scanResponse.updateScanResponseAfterBackup(),
),
canOverride = true,
)
// TODO [REDACTED_TASK_KEY] remove hot wallet after upgrade
userWalletCreated
}
}.requireColdWallet()
if (hasRing) {

View file

@ -0,0 +1,56 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.essenty.lifecycle.doOnStart
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.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.model.MultiWalletUpgradeWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.ui.MultiWalletUpgradeWallet
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
internal class MultiWalletUpgradeWalletComponent(
context: AppComponentContext,
params: MultiWalletChildParams,
onNextStep: (OnboardingMultiWalletState.Step) -> Unit,
) : AppComponentContext by context, MultiWalletChildComponent {
private val model: MultiWalletUpgradeWalletModel = getOrCreateModel(params)
init {
lifecycle.doOnStart {
params.innerNavigation.update {
it.copy(
stackSize = 2,
stackMaxSize = 9,
)
}
params.parentParams.titleProvider.changeTitle(
text = resourceReference(R.string.common_tangem),
)
}
componentScope.launch {
model.onDone.collect(onNextStep)
}
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
MultiWalletUpgradeWallet(
modifier = modifier,
state = state,
)
}
}

View file

@ -0,0 +1,181 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
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.ui.extensions.resourceReference
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.ExportSeedPhraseUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
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
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.ui.state.MultiWalletUpgradeWalletUM
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.Step
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class MultiWalletUpgradeWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val cardRepository: CardRepository,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val saveWalletUseCase: SaveWalletUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val exportSeedPhraseUseCase: ExportSeedPhraseUseCase,
) : Model() {
private val params = paramsContainer.require<MultiWalletChildParams>()
private val multiWalletState
get() = params.multiWalletState
private val _uiState = MutableStateFlow(
MultiWalletUpgradeWalletUM(
title = resourceReference(R.string.hw_upgrade_start_title),
bodyText = resourceReference(R.string.hw_upgrade_start_description),
onStartUpgradeClick = {
// TODO [REDACTED_TASK_KEY] track button click
upgradeWallet(false)
},
dialog = null,
),
)
val uiState: StateFlow<MultiWalletUpgradeWalletUM> = _uiState
val onDone = MutableSharedFlow<Step>()
init {
// TODO [REDACTED_TASK_KEY] track screen opened
}
private fun upgradeWallet(shouldReset: Boolean) {
modelScope.launch {
val mode = params.parentParams.mode
require(mode is OnboardingMultiWalletComponent.Mode.UpgradeHotWallet)
val userWallet = getUserWalletUseCase(mode.userWalletId)
.getOrElse { error("User wallet with id ${mode.userWalletId} not found") }
if (userWallet is UserWallet.Hot) {
val privateInfo = exportSeedPhraseUseCase
.invoke(userWallet.hotWalletId)
.getOrElse { error("Unable to export seed phrase for wallet with id ${mode.userWalletId}") }
modelScope.launch {
val result = tangemSdkManager.importWallet(
scanResponse = multiWalletState.value.currentScanResponse,
shouldReset = shouldReset,
mnemonic = privateInfo.mnemonic.mnemonicComponents.joinToString(" "),
passphrase = privateInfo.passphrase?.concatToString(),
)
when (result) {
is CompletionResult.Success -> {
multiWalletState.update {
it.copy(
currentScanResponse = it.currentScanResponse.copy(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
),
)
}
cardRepository.startCardActivation(cardId = result.data.card.cardId)
// TODO [REDACTED_TASK_KEY] track wallet created
val cardDoesNotSupportBackup = result.data.card.settings.isBackupAllowed.not()
when {
cardDoesNotSupportBackup -> createWalletAndNavigateBackWithDone()
params.parentParams.withSeedPhraseFlow -> onDone.emit(Step.AddBackupDevice)
else -> {
onDone.emit(Step.ChooseBackupOption)
}
}
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
// show should reset dialog
handleActivationError()
}
}
}
}
}
}
}
private fun createWalletAndNavigateBackWithDone() {
modelScope.launch {
val scanResponse = params.multiWalletState.value.currentScanResponse
val userWallet = createUserWallet(scanResponse)
saveWalletUseCase(userWallet, canOverride = true)
.onRight {
cardRepository.finishCardActivation(scanResponse.card.cardId)
// save user wallet for manage tokens screen
params.multiWalletState.update {
it.copy(resultUserWallet = userWallet)
}
onDone.emit(Step.Done)
}
}
}
private fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
return requireNotNull(
value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(),
lazyMessage = { "User wallet not created" },
)
}
private fun handleActivationError() {
_uiState.update { state ->
state.copy(
dialog = resetCardDialog(
onConfirm = ::navigateToSupportScreen,
dismiss = { _uiState.update { it.copy(dialog = null) } },
onDismissButtonClick = ::resetCard,
),
)
}
}
private fun resetCard() {
upgradeWallet(true)
}
fun navigateToSupportScreen() {
modelScope.launch {
val cardInfo =
getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))
}
}
}

View file

@ -0,0 +1,98 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
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.components.PrimaryButtonIconEnd
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
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.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.ui.state.MultiWalletUpgradeWalletUM
@Composable
internal fun MultiWalletUpgradeWallet(state: MultiWalletUpgradeWalletUM, modifier: Modifier = Modifier) {
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()
.navigationBarsPadding(),
verticalArrangement = Arrangement.Bottom,
) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(start = 32.dp, end = 32.dp, bottom = 16.dp)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = state.title.resolveReference(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp),
)
Text(
text = state.bodyText.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 12.dp),
)
}
PrimaryButtonIconEnd(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
iconResId = R.drawable.ic_tangem_24,
text = stringResourceSafe(R.string.hw_upgrade_start_action),
onClick = state.onStartUpgradeClick,
)
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
MultiWalletUpgradeWallet(
state = MultiWalletUpgradeWalletUM(
title = stringReference("Title"),
bodyText = stringReference("Body body body"),
onStartUpgradeClick = {},
dialog = null,
),
)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.ui.state
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
internal data class MultiWalletUpgradeWalletUM(
val title: TextReference,
val bodyText: TextReference,
val onStartUpgradeClick: () -> Unit,
val dialog: OnboardingDialogUM?,
)

View file

@ -11,6 +11,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.mod
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model.MultiWalletFinalizeModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.model.MultiWalletScanPrimaryModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.MultiWalletSeedPhraseModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.model.MultiWalletUpgradeWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel
import dagger.Binds
import dagger.Module
@ -72,4 +73,9 @@ internal interface ModelModule {
@IntoMap
@ClassKey(MultiWalletScanPrimaryModel::class)
fun provideModel8(model: MultiWalletScanPrimaryModel): Model
@Binds
@IntoMap
@ClassKey(MultiWalletUpgradeWalletModel::class)
fun provideModel9(model: MultiWalletUpgradeWalletModel): Model
}

View file

@ -117,6 +117,9 @@ internal class OnboardingMultiWalletModel @Inject constructor(
val card = scanResponse.card
return when {
params.mode is OnboardingMultiWalletComponent.Mode.UpgradeHotWallet -> {
OnboardingMultiWalletState.Step.UpgradeWallet
}
params.mode == OnboardingMultiWalletComponent.Mode.ContinueFinalize ->
OnboardingMultiWalletState.Step.Finalize
// Add backup button

View file

@ -32,7 +32,14 @@ data class OnboardingMultiWalletState(
* -> AddBackupDevice -> Finalize -> [Done]
*/
enum class Step {
CreateWallet, ChooseBackupOption, SeedPhrase, ScanPrimary, AddBackupDevice, Finalize, Done
UpgradeWallet,
CreateWallet,
ChooseBackupOption,
SeedPhrase,
ScanPrimary,
AddBackupDevice,
Finalize,
Done,
}
@JvmInline

View file

@ -14,4 +14,5 @@ fun screenTitleByStep(step: OnboardingMultiWalletState.Step): TextReference = wh
OnboardingMultiWalletState.Step.ChooseBackupOption -> resourceReference(R.string.onboarding_getting_started)
OnboardingMultiWalletState.Step.Finalize -> resourceReference(R.string.onboarding_button_finalize_backup)
OnboardingMultiWalletState.Step.Done -> resourceReference(R.string.common_done)
OnboardingMultiWalletState.Step.UpgradeWallet -> resourceReference(R.string.common_tangem)
}