Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-26 09:21:35 +03:00
parent a284c8d311
commit 7d017162cb
22 changed files with 554 additions and 111 deletions

View file

@ -14,6 +14,7 @@ android {
dependencies {
/** Api */
implementation(projects.features.onboardingV2.api)
implementation(projects.features.manageTokens.api)
/** Core modules */
implementation(projects.core.featuretoggles)
@ -34,6 +35,8 @@ dependencies {
implementation(projects.domain.core)
implementation(projects.domain.card)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.legacy)
/** Tangem libraries */
implementation(projects.libs.tangemSdkApi)

View file

@ -4,10 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.*
import com.arkivanov.decompose.value.Value
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
@ -51,20 +48,18 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
popBack = {
popInternal { success ->
if (success.not()) {
stackNavigation.pop()
model.stackNavigation.pop() // TODO
}
}
},
),
)
private val stackNavigation = StackNavigation<OnboardingRoute>()
private val innerStack: Value<ChildStack<OnboardingRoute, Any>> = childStack(
key = "innerStack",
source = stackNavigation,
source = model.stackNavigation,
serializer = null,
initialConfiguration = model.state.value.currentRoute,
initialConfiguration = model.startRoute,
handleBackButton = true,
childFactory = { configuration, factoryContext ->
onboardingChildFactory.createChild(

View file

@ -1,5 +1,7 @@
package com.tangem.features.onboarding.v2.entry.impl.model
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.navigate
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -7,9 +9,9 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.onboarding.v2.TitleProvider
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onboarding.v2.entry.impl.model.state.OnboardingState
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
@ -23,6 +25,7 @@ internal class OnboardingEntryModel @Inject constructor(
private val params = paramsContainer.require<OnboardingEntryComponent.Params>()
val stackNavigation = StackNavigation<OnboardingRoute>()
val titleProvider = object : TitleProvider {
override val currentTitle = MutableStateFlow(stringReference(""))
override fun changeTitle(text: TextReference) {
@ -30,11 +33,7 @@ internal class OnboardingEntryModel @Inject constructor(
}
}
val state = MutableStateFlow(
OnboardingState(
currentRoute = routeByProductType(params.scanResponse),
),
)
val startRoute = routeByProductType(params.scanResponse)
private fun routeByProductType(scanResponse: ScanResponse): OnboardingRoute {
return when (scanResponse.productType) {
@ -44,15 +43,24 @@ internal class OnboardingEntryModel @Inject constructor(
scanResponse = scanResponse,
withSeedPhraseFlow = false,
titleProvider = titleProvider,
onDone = ::onMultiWalletOnboardingDone,
)
ProductType.Wallet2 -> OnboardingRoute.MultiWallet(
ProductType.Ring,
ProductType.Wallet2,
-> OnboardingRoute.MultiWallet(
scanResponse = scanResponse,
withSeedPhraseFlow = true,
titleProvider = titleProvider,
onDone = ::onMultiWalletOnboardingDone,
)
ProductType.Start2Coin -> TODO()
ProductType.Ring -> TODO()
ProductType.Visa -> TODO()
}
}
private fun onMultiWalletOnboardingDone(userWallet: UserWallet) {
stackNavigation.navigate {
listOf(OnboardingRoute.ManageTokens(userWallet))
}
}
}

View file

@ -1,11 +1,14 @@
package com.tangem.features.onboarding.v2.entry.impl.routing
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import javax.inject.Inject
internal class OnboardingChildFactory @Inject constructor(
private val multiWalletComponentFactory: OnboardingMultiWalletComponent.Factory,
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
) {
fun createChild(route: OnboardingRoute, childContext: AppComponentContext): Any {
@ -16,6 +19,14 @@ internal class OnboardingChildFactory @Inject constructor(
scanResponse = route.scanResponse,
withSeedPhraseFlow = route.withSeedPhraseFlow,
titleProvider = route.titleProvider,
onDone = route.onDone,
),
)
is OnboardingRoute.ManageTokens -> manageTokensComponentFactory.create( // TODO replace with wrapper
context = childContext,
params = ManageTokensComponent.Params(
userWalletId = route.userWallet.walletId,
source = ManageTokensSource.ONBOARDING,
),
)
else -> Unit

View file

@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.entry.impl.routing
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.onboarding.v2.TitleProvider
sealed class OnboardingRoute : Route {
@ -12,5 +13,10 @@ sealed class OnboardingRoute : Route {
val titleProvider: TitleProvider,
val scanResponse: ScanResponse,
val withSeedPhraseFlow: Boolean,
val onDone: (UserWallet) -> Unit,
) : OnboardingRoute()
data class ManageTokens(
val userWallet: UserWallet,
) : OnboardingRoute()
}

View file

@ -8,6 +8,7 @@ import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.jetpack.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.onboarding.v2.entry.impl.routing.OnboardingRoute
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
@ -36,6 +37,11 @@ internal inline fun OnboardingEntry(
modifier = modifier,
)
}
is OnboardingRoute.ManageTokens -> {
(it.instance as ComposableContentComponent).Content(
modifier = modifier,
)
}
OnboardingRoute.None -> {}
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.core.decompose.factory.ComponentFactory
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.wallets.models.UserWallet
import com.tangem.features.onboarding.v2.TitleProvider
interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavigationHolder {
@ -12,6 +13,7 @@ interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavi
val titleProvider: TitleProvider,
val scanResponse: ScanResponse,
val withSeedPhraseFlow: Boolean,
val onDone: (UserWallet) -> Unit,
)
interface Factory : ComponentFactory<Params, OnboardingMultiWalletComponent>

View file

@ -22,13 +22,14 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerNavigation
import com.tangem.core.decompose.navigation.inner.InnerNavigationState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.MultiWalletAccessCodeComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.Wallet1ChooseOptionComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.MultiWalletCreateWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.MultiWalletFinalizeComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.MultiWalletSeedPhraseComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
@ -77,7 +78,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
}
}
private val childStack: Value<ChildStack<OnboardingMultiWalletState.Step, MultiWalletChildComponent>> =
private val childStack: Value<ChildStack<OnboardingMultiWalletState.Step, ComposableContentComponent>> =
childStack(
key = "innerStack",
source = stackNavigation,
@ -109,26 +110,23 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
init {
componentScope.launch {
// access code bs result
// TODO delete and use callback
childParams.multiWalletState
.map { it.accessCode }
.distinctUntilChanged()
.filter { it != null }
.collectLatest { handleNavigationEvent(Finalize) }
}
bottomSheetNavigation.navigate {}
}
private fun createChild(
step: OnboardingMultiWalletState.Step,
childContext: AppComponentContext,
): MultiWalletChildComponent {
): ComposableContentComponent {
return when (step) {
CreateWallet -> MultiWalletCreateWalletComponent(
context = childContext,
params = MultiWalletChildParams(
multiWalletState = model.state,
parentParams = params,
),
params = childParams,
onNextStep = ::handleNavigationEvent,
)
ChooseBackupOption -> Wallet1ChooseOptionComponent(
@ -137,23 +135,21 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
)
SeedPhrase -> MultiWalletSeedPhraseComponent(
context = childContext,
params = MultiWalletChildParams(
multiWalletState = model.state,
parentParams = params,
),
params = childParams,
onNextStep = ::handleNavigationEvent,
backButtonClickFlow = MutableSharedFlow(),
backButtonClickFlow = MutableSharedFlow(), // TODO
)
AddBackupDevice -> MultiWalletBackupComponent(
context = childContext,
params = MultiWalletChildParams(
multiWalletState = model.state,
parentParams = params,
),
params = childParams,
onEvent = ::handleBackupComponentEvent,
)
Finalize -> TODO()
Done -> TODO()
Finalize -> MultiWalletFinalizeComponent(
context = childContext,
params = childParams,
onEvent = ::handleFinalizeComponentEvent,
)
Done -> error("Unexpected Done state")
}
}
@ -162,15 +158,17 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
ChooseBackupOption -> {
artworksState.value = WalletArtworksState.Fan
}
SeedPhrase -> {}
AddBackupDevice -> {
artworksState.value = WalletArtworksState.Unfold(WalletArtworksState.Unfold.Step.First)
}
Finalize -> {
artworksState.value = WalletArtworksState.Stack()
artworksState.value = WalletArtworksState.Stack(threeCards = model.state.value.isThreeCards)
}
Done -> {
// TODO
// final step - navigate to parent
val userWallet = childParams.multiWalletState.value.resultUserWallet ?: return
params.onDone(userWallet)
return
}
else -> return
}
@ -179,19 +177,44 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
private fun handleBackupComponentEvent(event: MultiWalletBackupComponent.Event) {
when (event) {
MultiWalletBackupComponent.Event.Done -> {
artworksState.value = WalletArtworksState.Stack()
bottomSheetNavigation.navigate { }
}
MultiWalletBackupComponent.Event.OneDeviceAdded -> {
artworksState.value = WalletArtworksState.Unfold(WalletArtworksState.Unfold.Step.Second)
}
MultiWalletBackupComponent.Event.TwoDeviceAdded -> {
artworksState.value = WalletArtworksState.Unfold(WalletArtworksState.Unfold.Step.Third)
}
MultiWalletBackupComponent.Event.Done -> {
openAccessCodeBottomSheet()
}
}
}
private fun handleFinalizeComponentEvent(event: MultiWalletFinalizeComponent.Event) {
when (event) {
MultiWalletFinalizeComponent.Event.OneBackupCardAdded -> {
artworksState.value = WalletArtworksState.Leapfrog(
threeCards = model.state.value.isThreeCards,
step = WalletArtworksState.Leapfrog.Step.Second,
)
}
MultiWalletFinalizeComponent.Event.TwoBackupCardsAdded -> {
if (model.state.value.isThreeCards) {
artworksState.value = WalletArtworksState.Leapfrog(
threeCards = model.state.value.isThreeCards,
step = WalletArtworksState.Leapfrog.Step.Third,
)
} else {
handleNavigationEvent(Done)
}
}
MultiWalletFinalizeComponent.Event.ThreeBackupCardsAdded -> {
handleNavigationEvent(Done)
}
}
}
private fun openAccessCodeBottomSheet() { bottomSheetNavigation.navigate { } }
@OptIn(FaultyDecomposeApi::class)
@Composable
override fun Content(modifier: Modifier) {

View file

@ -7,6 +7,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state.MultiWalletAccessCodeUM
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@ -21,6 +22,7 @@ private const val MINIMUM_ACCESS_CODE_LENGTH = 4
internal class MultiWalletAccessCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val backupServiceHolder: BackupServiceHolder,
) : Model() {
private val _uiState = MutableStateFlow(getInitialState())
@ -112,6 +114,10 @@ internal class MultiWalletAccessCodeModel @Inject constructor(
}
MultiWalletAccessCodeUM.Step.ConfirmAccessCode -> {
if (checkAccessCode()) {
backupServiceHolder.backupService.get()?.setAccessCode(_uiState.value.accessCodeFirst.text)
// TODO delete access code from state
// make on done
params.multiWalletState.update {
it.copy(accessCode = OnboardingMultiWalletState.AccessCode(uiState.value.accessCodeFirst.text))
}

View file

@ -1,48 +1,6 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model
import android.graphics.Bitmap
import com.tangem.common.card.Card
// TODO maybe delete some fields in the future
data class BackupState(
val primaryCardId: String? = null,
val primaryCardBatchId: String? = null,
val backupCardsNumber: Int = 0,
val backupCards: List<Card> = emptyList(),
val backupCardIds: List<CardId> = emptyList(),
val backupCardBatchIds: List<CardId> = emptyList(),
@Transient
val backupCardsArtworks: Map<CardId, Bitmap> = emptyMap(),
val accessCode: String = "",
val accessCodeError: AccessCodeError? = null,
val backupStep: BackupStep? = null,
val maxBackupCards: Int = 2,
val canSkipBackup: Boolean = true,
val isInterruptedBackup: Boolean = false,
val showBtnLoading: Boolean = false,
val hasBackupError: Boolean = false,
val startedSource: BackupStartedSource = BackupStartedSource.Onboarding,
val hasRing: Boolean = false,
)
enum class BackupStartedSource {
Onboarding, CreateBackup
}
enum class AccessCodeError {
CodeTooShort, CodesDoNotMatch
}
typealias CardId = String
sealed class BackupStep {
data object InitBackup : BackupStep()
data object ScanOriginCard : BackupStep()
data object AddBackupCards : BackupStep()
data object SetAccessCode : BackupStep()
data object EnterAccessCode : BackupStep()
data object ReenterAccessCode : BackupStep()
data object WritePrimaryCard : BackupStep()
data class WriteBackupCard(val cardNumber: Int) : BackupStep()
data object Finished : BackupStep()
}
)

View file

@ -67,7 +67,7 @@ class MultiWalletBackupModel @Inject constructor(
addBackupButtonEnabled = true,
addBackupButtonLoading = false,
onAddBackupClick = ::startBackupWallet,
onFinalizeButtonClick = {},
onFinalizeButtonClick = ::onFinalizeClick,
onSkipButtonClick = {
// eventFlow.tryEmit(Unit)
},
@ -80,7 +80,9 @@ class MultiWalletBackupModel @Inject constructor(
}
private fun startBackupWallet() {
backupService.discardSavedBackup()
if (state.value.backupCardsNumber == 0) {
backupService.discardSavedBackup()
}
val primaryCard = scanResponse.primaryCard
if (primaryCard != null) {
@ -107,6 +109,7 @@ class MultiWalletBackupModel @Inject constructor(
modelScope.launch {
eventFlow.emit(MultiWalletBackupComponent.Event.OneDeviceAdded)
}
params.multiWalletState.update { it.copy(isThreeCards = false) }
st.copy(
title = resourceReference(R.string.onboarding_title_one_backup_card),
bodyText = resourceReference(R.string.onboarding_subtitle_one_backup_card),
@ -118,6 +121,7 @@ class MultiWalletBackupModel @Inject constructor(
modelScope.launch {
eventFlow.emit(MultiWalletBackupComponent.Event.TwoDeviceAdded)
}
params.multiWalletState.update { it.copy(isThreeCards = true) }
st.copy(
title = resourceReference(R.string.onboarding_title_two_backup_cards),
bodyText = resourceReference(R.string.onboarding_subtitle_two_backup_cards),
@ -130,6 +134,19 @@ class MultiWalletBackupModel @Inject constructor(
}
}
private fun onFinalizeClick() {
when (state.value.backupCardsNumber) {
1 -> {
// TODO show dialog
params.multiWalletState.update { it.copy(isThreeCards = false) }
}
2 -> {
params.multiWalletState.update { it.copy(isThreeCards = true) }
}
}
modelScope.launch { eventFlow.emit(MultiWalletBackupComponent.Event.Done) }
}
private fun addBackupCardWithService() {
_uiState.update { st ->
st.copy(addBackupButtonLoading = true)
@ -147,7 +164,6 @@ class MultiWalletBackupModel @Inject constructor(
is CompletionResult.Success -> {
state.update {
it.copy(
backupCards = it.backupCards + result.data,
backupCardsNumber = it.backupCardsNumber + 1,
)
}

View file

@ -1,22 +1,45 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize
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.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import kotlinx.coroutines.flow.SharedFlow
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model.MultiWalletFinalizeModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui.MultiWalletFinalize
import kotlinx.coroutines.launch
@Suppress("UnusedPrivateMember")
internal class MultiWalletFinalizeComponent(
context: AppComponentContext,
params: MultiWalletChildParams,
onNextStep: (OnboardingMultiWalletState.Step) -> Unit,
backButtonClickFlow: SharedFlow<Unit>,
onEvent: (Event) -> Unit,
) : AppComponentContext by context, MultiWalletChildComponent {
private val model: MultiWalletFinalizeModel = getOrCreateModel(params)
init {
componentScope.launch {
model.onEvent.collect {
onEvent(it)
}
}
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
MultiWalletFinalize(
state = state,
modifier = modifier,
)
}
enum class Event {
OneBackupCardAdded, TwoBackupCardsAdded, ThreeBackupCardsAdded
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model
import com.tangem.common.card.EllipticCurve
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.models.scan.CardDTO
import javax.inject.Inject
class BackupValidator @Inject constructor() {
fun isValidFull(cardDTO: CardDTO): Boolean {
return validateBackupStatus(cardDTO) && validateCurves(cardDTO)
}
fun isValidBackupStatus(cardDTO: CardDTO): Boolean {
return validateBackupStatus(cardDTO)
}
private fun validateCurves(cardDTO: CardDTO): Boolean {
val config = CardConfig.createConfig(cardDTO)
// / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets,
// we cannot determine whether this curve is missing due to an error or because the user
// did not want to recreate the wallet.
val expectedCurves = config.mandatoryCurves
.filterNot { it == EllipticCurve.Bls12381G2Aug }
val curves = cardDTO.wallets.map { it.curve }
for (expectedCurve in expectedCurves) {
val cardCurvesCount = curves.count { it == expectedCurve }
// missing curve
if (cardCurvesCount == 0) {
return false
}
// duplicated curve
if (cardCurvesCount > 1) {
return false
}
}
return true
}
private fun validateBackupStatus(cardDTO: CardDTO): Boolean {
val backupStatus = cardDTO.backupStatus
backupStatus ?: return true // for card with null backup status, validation should always returns true
return backupStatus !is CardDTO.BackupStatus.CardLinked
}
}

View file

@ -0,0 +1,257 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.scan.isRing
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
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.finalize.MultiWalletFinalizeComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui.state.MultiWalletFinalizeUM
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.StringsSigns
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@ComponentScoped
internal class MultiWalletFinalizeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val backupServiceHolder: BackupServiceHolder,
private val tangemSdkManager: TangemSdkManager,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
private val userWalletsListManager: UserWalletsListManager,
private val cardRepository: CardRepository,
) : Model() {
private val params = paramsContainer.require<MultiWalletChildParams>()
private val multiWalletState get() = params.multiWalletState
private val _uiState = MutableStateFlow(
MultiWalletFinalizeUM(
isRing = backupServiceHolder.backupService.get()?.primaryCardBatchId?.let { isRing(it) } ?: false,
onScanClick = ::onLinkClick,
cardNumber = backupServiceHolder.backupService.get()?.primaryCardId?.lastMasked().orEmpty(),
),
)
private val backupCardIds = backupServiceHolder.backupService.get()?.backupCardIds.orEmpty()
private var walletHasBackupError = false
val uiState = _uiState.asStateFlow()
val onEvent = MutableSharedFlow<MultiWalletFinalizeComponent.Event>()
private fun onLinkClick() {
when (_uiState.value.step) {
MultiWalletFinalizeUM.Step.Primary -> {
writePrimaryCard()
}
MultiWalletFinalizeUM.Step.BackupDevice1 -> {
writeBackupCard(cardIndex = 0)
}
MultiWalletFinalizeUM.Step.BackupDevice2 -> {
writeBackupCard(cardIndex = 1)
}
}
}
private fun writePrimaryCard() {
val backupService = backupServiceHolder.backupService.get() ?: return
// store.dispatchOnMain(BackupAction.SetHasRing(hasRing = isRing))
val primaryCardBatchId = backupService.primaryCardBatchId ?: return
val isRing = isRing(primaryCardBatchId)
val iconScanRes = if (isRing) R.drawable.img_hand_scan_ring else null
tangemSdkManager.changeProductType(isRing)
backupService.proceedBackup(iconScanRes = iconScanRes) { result ->
when (result) {
is CompletionResult.Success -> {
modelScope.launch { onEvent.emit(MultiWalletFinalizeComponent.Event.OneBackupCardAdded) }
_uiState.update { st ->
st.copy(
step = MultiWalletFinalizeUM.Step.BackupDevice1,
isRing = backupService.backupCardsBatchIds.getOrNull(0)?.let { isRing(it) } ?: false,
scanPrimary = false,
cardNumber = backupService.backupCardIds.firstOrNull()?.lastMasked() ?: "",
)
}
}
is CompletionResult.Failure -> Unit
}
tangemSdkManager.clearProductType()
}
}
private fun writeBackupCard(cardIndex: Int) {
val backupService = backupServiceHolder.backupService.get() ?: return
// store.dispatchOnMain(BackupAction.SetHasRing(hasRing = isRing))
val backupCardBatchId = backupService.backupCardsBatchIds.getOrNull(cardIndex) ?: return
val isRing = isRing(backupCardBatchId)
val iconScanRes = if (isRing) R.drawable.img_hand_scan_ring else null
tangemSdkManager.changeProductType(isRing)
backupService.proceedBackup(iconScanRes = iconScanRes) { result ->
when (result) {
is CompletionResult.Success -> {
val backupValidator = BackupValidator()
if (backupValidator.isValidBackupStatus(CardDTO(result.data)).not()) {
// TODO store.dispatchOnMain(BackupAction.ErrorInBackupCard)
walletHasBackupError = true
}
if (backupService.currentState == BackupService.State.Finished) {
finishBackup()
} else {
modelScope.launch { onEvent.emit(MultiWalletFinalizeComponent.Event.TwoBackupCardsAdded) }
_uiState.update { st ->
st.copy(
step = MultiWalletFinalizeUM.Step.BackupDevice2,
isRing = backupService.backupCardsBatchIds
.getOrNull(cardIndex + 1)?.let { isRing(it) } ?: false,
cardNumber = backupService.backupCardIds.getOrNull(cardIndex + 1)
?.lastMasked() ?: "",
)
}
}
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
// show should reset dialog
handleActivationError()
}
}
}
tangemSdkManager.clearProductType()
}
}
private fun finishBackup() {
// TODO Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
modelScope.launch {
val scanResponse = params.multiWalletState.value.currentScanResponse
val userWallet = createUserWallet(scanResponse)
userWalletsListManager.save(
userWallet = userWallet.copy(
scanResponse = scanResponse.updateScanResponseAfterBackup(),
),
canOverride = true,
)
// TODO
// BackupStartedSource.CreateBackup -> updateWallet(
// userWalletsListManager = userWalletsListManager,
// userWallet = userWallet,
// backupState = backupState,
// )
// TODO maybe here we should check for backup cards activation status
// if (notActivatedCardIds.isEmpty()) {
// delay(1000)
// store.dispatchWithMain(BackupAction.BackupFinished(userWallet?.walletId))
// return@launch
// }
//
// Analytics.send(Onboarding.Finished())
//
// store.state.globalState.onboardingState.onboardingManager?.finishActivation(notActivatedCardIds)
// handleFinishBackup(requireNotNull(scanResponse), userWallet)
// delay(1000)
// store.dispatchWithMain(BackupAction.BackupFinished(userWalletId = userWallet?.walletId))
cardRepository.finishCardActivation(scanResponse.card.cardId)
// save user wallet for manage tokens screen
params.multiWalletState.update {
it.copy(resultUserWallet = userWallet)
}
onEvent.emit(MultiWalletFinalizeComponent.Event.ThreeBackupCardsAdded)
}
}
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet {
return requireNotNull(
value = UserWalletBuilder(scanResponse, generateWalletNameUseCase)
.backupCardsIds(backupCardIds.toSet())
.hasBackupError(walletHasBackupError)
.build(),
lazyMessage = { "User wallet not created" },
)
}
@Suppress("MagicNumber")
private fun ScanResponse.updateScanResponseAfterBackup(): ScanResponse {
val cardsCount = if (params.multiWalletState.value.isThreeCards) 3 else 2
val card = card.copy(
backupStatus = CardDTO.BackupStatus.Active(cardCount = cardsCount),
isAccessCodeSet = true,
)
return copy(card = card)
}
private fun handleActivationError() {
_uiState.update {
it.copy(
dialog = resetCardDialog(
onConfirm = ::navigateToSupportScreen,
dismiss = { _uiState.update { it.copy(dialog = null) } },
onDismissButtonClick = ::resetCard,
),
)
}
}
private fun resetCard() {
val scanResponse = params.parentParams.scanResponse
modelScope.launch {
tangemSdkManager.resetToFactorySettings(
cardId = scanResponse.card.cardId,
allowsRequestAccessCodeFromRepository = true,
)
}
}
private fun navigateToSupportScreen() {
modelScope.launch {
val cardInfo = getCardInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))
}
}
private fun String.lastMasked(): String {
val space = StringsSigns.NON_BREAKING_SPACE
fun String.splitBySpace() = reversed().chunked(size = 4).joinToString(separator = "$space").reversed()
val last4 = takeLast(n = 4).splitBySpace()
val mask = "$space*$space*$space*$space"
return mask + last4
}
}

View file

@ -1,10 +0,0 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.state
data class MultiWalletFinalizeUM(
val step: Step = Step.Device1,
val cardNumber: String = "",
) {
enum class Step {
Device1, Device2, Device3
}
}

View file

@ -1,22 +1,86 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.state.MultiWalletFinalizeUM
import androidx.compose.ui.res.stringResource
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.PrimaryButtonIconEnd
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.finalize.ui.state.MultiWalletFinalizeUM
@Suppress("UnusedPrivateMember")
@Composable
fun MultiWalletFinalize(state: MultiWalletFinalizeUM, modifier: Modifier = Modifier) {
internal fun MultiWalletFinalize(state: MultiWalletFinalizeUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding(),
verticalArrangement = Arrangement.Bottom,
) {
// TODO
Column(
Modifier
.fillMaxWidth()
.padding(start = 32.dp, end = 32.dp, bottom = 74.dp)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(top = 16.dp),
text = stringResource(
when {
state.scanPrimary && state.isRing -> R.string.common_origin_ring
state.scanPrimary && state.isRing.not() -> R.string.common_origin_card
state.scanPrimary.not() && state.isRing -> R.string.onboarding_title_backup_ring
else -> R.string.onboarding_title_backup_card
},
),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.h2,
)
Text(
modifier = Modifier.padding(top = 12.dp),
text = if (state.isRing) {
stringResource(R.string.onboarding_subtitle_scan_ring)
} else {
stringResource(
R.string.onboarding_subtitle_scan_backup_card_format,
state.cardNumber,
)
},
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
style = TangemTheme.typography.body1,
)
}
PrimaryButtonIconEnd(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
text = stringResource(
if (state.isRing) R.string.onboarding_button_backup_ring else R.string.onboarding_button_backup_origin,
),
iconResId = R.drawable.ic_tangem_24,
onClick = state.onScanClick,
)
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
MultiWalletFinalize(
state = MultiWalletFinalizeUM(),
)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui.state
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
internal data class MultiWalletFinalizeUM(
val step: Step = Step.Primary,
val isRing: Boolean = false,
val scanPrimary: Boolean = true,
val cardNumber: String = "",
val onScanClick: () -> Unit = {},
val dialog: OnboardingDialogUM? = null,
) {
enum class Step {
Primary, BackupDevice1, BackupDevice2
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.DefaultOnboardingMulti
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.model.MultiWalletAccessCodeModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model.MultiWalletBackupModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.model.MultiWalletCreateWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model.MultiWalletFinalizeModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.MultiWalletSeedPhraseModel
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel
import dagger.Binds
@ -54,4 +55,9 @@ internal interface ModelModule {
@IntoMap
@ClassKey(MultiWalletAccessCodeModel::class)
fun provideModel5(model: MultiWalletAccessCodeModel): Model
@Binds
@IntoMap
@ClassKey(MultiWalletFinalizeModel::class)
fun provideModel6(model: MultiWalletFinalizeModel): Model
}

View file

@ -26,6 +26,8 @@ internal class OnboardingMultiWalletModel @Inject constructor(
currentStep = getInitialStep(),
currentScanResponse = params.scanResponse,
accessCode = null,
isThreeCards = true,
resultUserWallet = null,
),
)

View file

@ -1,11 +1,14 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.model
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
data class OnboardingMultiWalletState(
val currentStep: Step,
val accessCode: AccessCode?,
val isThreeCards: Boolean,
val currentScanResponse: ScanResponse,
val resultUserWallet: UserWallet?, // TODO make callback instead
) {
/**
* Wallet1

View file

@ -12,5 +12,5 @@ fun screenTitleByStep(step: OnboardingMultiWalletState.Step): TextReference = wh
OnboardingMultiWalletState.Step.AddBackupDevice ->
resourceReference(R.string.onboarding_navbar_title_creating_backup)
OnboardingMultiWalletState.Step.Finalize -> TODO()
OnboardingMultiWalletState.Step.Done -> TODO()
OnboardingMultiWalletState.Step.Done -> error("No title for Done step")
}

View file

@ -31,6 +31,7 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor(
init {
componentScope.launch {
state.collect {
// TODO
Timber.tag("ASDASD").d("State: $it")
}
}