Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-05 10:35:27 +03:00
parent 022c4bcfcd
commit d71d29b72b
30 changed files with 295 additions and 104 deletions

View file

@ -25,6 +25,7 @@ dependencies {
implementation(projects.core.res)
implementation(projects.core.decompose)
implementation(projects.core.navigation)
implementation(projects.core.datasource)
/** Common */
implementation(projects.common.ui)
@ -39,6 +40,7 @@ dependencies {
implementation(projects.domain.wallets.models)
implementation(projects.domain.legacy)
implementation(projects.domain.settings)
implementation(projects.domain.onboarding)
/** Tangem libraries */
implementation(projects.libs.tangemSdkApi)
@ -62,6 +64,7 @@ dependencies {
implementation(deps.lottie.compose)
implementation(deps.decompose.ext.compose)
implementation(deps.androidx.activity.compose)
implementation(deps.androidx.datastore)
/** Other libraries */
implementation(deps.kotlin.immutable.collections)

View file

@ -53,8 +53,6 @@ internal class OnboardingEntryModel @Inject constructor(
}
return when (scanResponse.productType) {
ProductType.Note -> TODO()
ProductType.Twins -> TODO()
ProductType.Wallet -> OnboardingRoute.MultiWallet(
scanResponse = scanResponse,
withSeedPhraseFlow = false,
@ -71,8 +69,7 @@ internal class OnboardingEntryModel @Inject constructor(
onDone = ::onMultiWalletOnboardingDone,
mode = multiWalletNavigationMode,
)
ProductType.Start2Coin -> TODO()
ProductType.Visa -> TODO()
else -> error("Unsupported")
}
}

View file

@ -123,7 +123,6 @@ internal class MultiWalletAccessCodeModel @Inject constructor(
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,6 +1,5 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model
// TODO maybe delete some fields in the future
data class BackupState(
val backupCardsNumber: Int = 0,
val numberOfBackupCards: Int = 0,
)

View file

@ -15,9 +15,9 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEv
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.backupCardAttestationFailedDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.onlyOneBackupDeviceDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.resetBackupCardDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state.MultiWalletBackupUM
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -70,30 +70,19 @@ class MultiWalletBackupModel @Inject constructor(
}
private fun getInitState(): MultiWalletBackupUM {
return when (backupService.currentState) {
BackupService.State.Preparing -> {
MultiWalletBackupUM(
title = resourceReference(R.string.onboarding_title_no_backup_cards),
bodyText = resourceReference(R.string.onboarding_subtitle_no_backup_cards),
finalizeButtonEnabled = false,
addBackupButtonEnabled = true,
addBackupButtonLoading = false,
onAddBackupClick = ::startBackupWallet,
onFinalizeButtonClick = ::onFinalizeClick,
onSkipButtonClick = {
// eventFlow.tryEmit(Unit)
},
)
}
// TODO
is BackupService.State.FinalizingBackupCard -> TODO()
BackupService.State.FinalizingPrimaryCard -> TODO()
BackupService.State.Finished -> TODO()
}
return MultiWalletBackupUM(
title = resourceReference(R.string.onboarding_title_no_backup_cards),
bodyText = resourceReference(R.string.onboarding_subtitle_no_backup_cards),
finalizeButtonEnabled = false,
addBackupButtonEnabled = true,
addBackupButtonLoading = false,
onAddBackupClick = ::startBackupWallet,
onFinalizeButtonClick = ::onFinalizeClick,
)
}
private fun startBackupWallet() {
if (state.value.backupCardsNumber == 0) {
if (state.value.numberOfBackupCards == 0) {
backupService.discardSavedBackup()
}
val primaryCard = scanResponse.primaryCard
@ -144,19 +133,35 @@ class MultiWalletBackupModel @Inject constructor(
}
}
private fun onFinalizeClick() {
when (state.value.backupCardsNumber) {
private fun onFinalizeClick(fromDialog: Boolean = false) {
when (state.value.numberOfBackupCards) {
1 -> {
// TODO show dialog
params.multiWalletState.update { it.copy(isThreeCards = false) }
if (fromDialog.not()) {
showOnlyOneBackupWarningDialog()
return
}
}
2 -> {
params.multiWalletState.update { it.copy(isThreeCards = true) }
}
}
modelScope.launch { eventFlow.emit(MultiWalletBackupComponent.Event.Done) }
analyticsHandler.send(OnboardingEvent.Backup.Finished(cardsCount = state.value.backupCardsNumber + 1))
analyticsHandler.send(OnboardingEvent.Backup.Finished(cardsCount = state.value.numberOfBackupCards + 1))
}
private fun showOnlyOneBackupWarningDialog() {
_uiState.update {
it.copy(
dialog = onlyOneBackupDeviceDialog(
onDismiss = { _uiState.update { it.copy(dialog = null) } },
onConfirm = { onFinalizeClick(fromDialog = true) },
),
)
}
}
private fun addBackupCardWithService() {
@ -176,7 +181,7 @@ class MultiWalletBackupModel @Inject constructor(
is CompletionResult.Success -> {
state.update {
it.copy(
backupCardsNumber = it.backupCardsNumber + 1,
numberOfBackupCards = it.numberOfBackupCards + 1,
)
}
@ -186,12 +191,12 @@ class MultiWalletBackupModel @Inject constructor(
)
params.backups.update {
it.copy(
card2 = if (state.value.backupCardsNumber == 1) backupCardInfo else it.card2,
card3 = if (state.value.backupCardsNumber == 2) backupCardInfo else it.card3,
card2 = if (state.value.numberOfBackupCards == 1) backupCardInfo else it.card2,
card3 = if (state.value.numberOfBackupCards == 2) backupCardInfo else it.card3,
)
}
setNumberOfBackupCards(state.value.backupCardsNumber)
setNumberOfBackupCards(state.value.numberOfBackupCards)
}
is CompletionResult.Failure -> {
when (val error = result.error) {

View file

@ -28,4 +28,18 @@ internal fun backupCardAttestationFailedDialog(onDismiss: () -> Unit) = MultiWal
onDismiss = onDismiss,
onConfirm = onDismiss,
onCancel = null,
)
internal fun onlyOneBackupDeviceDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) = MultiWalletBackupUMDialog(
title = resourceReference(R.string.common_warning),
bodyText = resourceReference(R.string.onboarding_alert_message_not_max_backup_cards_added),
confirmText = resourceReference(R.string.common_continue),
cancelText = resourceReference(R.string.common_cancel),
warningCancelColor = false,
onDismiss = onDismiss,
onConfirm = {
onDismiss()
onConfirm()
},
onCancel = onDismiss,
)

View file

@ -13,7 +13,6 @@ data class MultiWalletBackupUM(
val finalizeButtonEnabled: Boolean = false,
val onAddBackupClick: () -> Unit = {},
val onFinalizeButtonClick: () -> Unit = {},
val onSkipButtonClick: () -> Unit = {},
val dialog: MultiWalletBackupUMDialog? = null,
)

View file

@ -37,8 +37,6 @@ internal class Wallet1ChooseOptionModel @Inject constructor(
}
fun onSkipClick() {
// TODO show confirmation dialog
if (skipClicked) return
skipClicked = true

View file

@ -12,6 +12,7 @@ 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.onboarding.repository.OnboardingRepository
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
@ -22,6 +23,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChild
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.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.FinalizeStage.*
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.sdk.api.TangemSdkManager
@ -46,23 +48,66 @@ internal class MultiWalletFinalizeModel @Inject constructor(
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
private val userWalletsListManager: UserWalletsListManager,
private val cardRepository: CardRepository,
private val onboardingRepository: OnboardingRepository,
) : 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
private val _uiState = MutableStateFlow(getInitialState())
private val backupCardIds = backupServiceHolder.backupService.get()?.backupCardIds.orEmpty()
private var walletHasBackupError = false
val uiState = _uiState.asStateFlow()
val onEvent = MutableSharedFlow<MultiWalletFinalizeComponent.Event>()
init {
// save scan response to preferences to be able
// to continue finalize process after app restart
modelScope.launch {
onboardingRepository.saveUnfinishedFinalizeOnboarding(
scanResponse = multiWalletState.value.currentScanResponse,
)
}
}
private fun getInitialState(): MultiWalletFinalizeUM {
val backupService = backupServiceHolder.backupService.get() ?: return MultiWalletFinalizeUM()
val initialStep = getInitialStep()
val batchId = when (initialStep) {
MultiWalletFinalizeUM.Step.Primary -> backupService.primaryCardBatchId
MultiWalletFinalizeUM.Step.BackupDevice1 -> backupService.backupCardsBatchIds.getOrNull(0)
MultiWalletFinalizeUM.Step.BackupDevice2 -> backupService.backupCardsBatchIds.getOrNull(1)
}
val cardId = when (initialStep) {
MultiWalletFinalizeUM.Step.Primary -> backupService.primaryCardId
MultiWalletFinalizeUM.Step.BackupDevice1 -> backupService.backupCardIds.getOrNull(0)
MultiWalletFinalizeUM.Step.BackupDevice2 -> backupService.backupCardIds.getOrNull(1)
}
return MultiWalletFinalizeUM(
isRing = batchId?.let { isRing(it) } ?: false,
onScanClick = ::onLinkClick,
scanPrimary = initialStep == MultiWalletFinalizeUM.Step.Primary,
cardNumber = cardId?.lastMasked().orEmpty(),
step = getInitialStep(),
)
}
private fun getInitialStep(): MultiWalletFinalizeUM.Step {
val startFromFinalize =
params.multiWalletState.value.startFromFinalize ?: return MultiWalletFinalizeUM.Step.Primary
return when (startFromFinalize) {
ScanPrimaryCard -> MultiWalletFinalizeUM.Step.Primary
ScanBackupFirstCard -> MultiWalletFinalizeUM.Step.BackupDevice1
ScanBackupSecondCard -> MultiWalletFinalizeUM.Step.BackupDevice2
}
}
private fun onLinkClick() {
when (_uiState.value.step) {
MultiWalletFinalizeUM.Step.Primary -> {
@ -79,8 +124,6 @@ internal class MultiWalletFinalizeModel @Inject constructor(
private fun writePrimaryCard() {
val backupService = backupServiceHolder.backupService.get() ?: return
// TODO 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
@ -107,9 +150,6 @@ internal class MultiWalletFinalizeModel @Inject constructor(
private fun writeBackupCard(cardIndex: Int) {
val backupService = backupServiceHolder.backupService.get() ?: return
// TODO 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
@ -151,8 +191,6 @@ internal class MultiWalletFinalizeModel @Inject constructor(
}
private fun finishBackup() {
// TODO Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
modelScope.launch {
val scanResponse = params.multiWalletState.value.currentScanResponse
val userWallet = createUserWallet(scanResponse)
@ -183,8 +221,12 @@ internal class MultiWalletFinalizeModel @Inject constructor(
it.copy(resultUserWallet = userWallet)
}
cardRepository.finishCardActivation(scanResponse.card.cardId)
// user wallet is fully created and saved, remove scan response from preferences
// to prevent showing finalize screen dialog on next app start
onboardingRepository.clearUnfinishedFinalizeOnboarding()
cardRepository.finishCardActivation(scanResponse.card.cardId)
backupServiceHolder.backupService.get()?.discardSavedBackup()
onEvent.emit(MultiWalletFinalizeComponent.Event.ThreeBackupCardsAdded)
}
}
@ -201,7 +243,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
@Suppress("MagicNumber")
private fun ScanResponse.updateScanResponseAfterBackup(): ScanResponse {
val cardsCount = if (params.multiWalletState.value.isThreeCards) 3 else 2
val cardsCount = if (params.multiWalletState.value.isThreeCards) 2 else 1
val card = card.copy(
backupStatus = CardDTO.BackupStatus.Active(cardCount = cardsCount),

View file

@ -1,7 +1,7 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

View file

@ -135,8 +135,6 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
}
private fun openGeneratedSeedPhrase() {
// TODO analytics SeedPhraseEvents.ButtonGenerateSeedPhrase
val words12 = state.value.generatedWords12 ?: return
val words24 = state.value.generatedWords24 ?: return

View file

@ -5,13 +5,18 @@ import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.interruptBackupDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.FinalizeStage
import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -23,6 +28,8 @@ internal class OnboardingMultiWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val analyticsHandler: AnalyticsEventHandler,
private val backupServiceHolder: BackupServiceHolder,
private val appPreferencesStore: AppPreferencesStore,
) : Model() {
private val params = paramsContainer.require<OnboardingMultiWalletComponent.Params>()
private val getCardImageUseCase = GetCardImageUseCase()
@ -35,6 +42,7 @@ internal class OnboardingMultiWalletModel @Inject constructor(
accessCode = null,
isThreeCards = true,
resultUserWallet = null,
startFromFinalize = getInitialStartFromFinalize(),
),
)
val backups = MutableStateFlow(MultiWalletChildParams.Backup())
@ -48,10 +56,11 @@ internal class OnboardingMultiWalletModel @Inject constructor(
}
fun onBack() {
_uiState.update {
it.copy(
_uiState.update { st ->
st.copy(
dialog = interruptBackupDialog(
onConfirm = {
removeFinalizeScanResponseState()
router.pop()
},
dismiss = { _uiState.update { it.copy(dialog = null) } },
@ -60,20 +69,30 @@ internal class OnboardingMultiWalletModel @Inject constructor(
}
}
private fun removeFinalizeScanResponseState() {
// discarding onboarding means we have to remove scan response from preferences
// to prevent showing finalize screen dialog on next app start
modelScope.launch {
appPreferencesStore.editData { mutablePreferences ->
mutablePreferences.remove(PreferencesKeys.ONBOARDING_FINALIZE_SCAN_RESPONSE_KEY)
}
}
}
private fun subscribeToBackups() {
modelScope.launch {
backups.collectLatest { bst ->
backups.collectLatest { backup ->
when {
_uiState.value.artwork2Url == null && bst.card2 != null -> {
_uiState.value.artwork2Url == null && backup.card2 != null -> {
val artwork =
getCardImageUseCase.invoke(bst.card2.cardId, bst.card2.cardPublicKey)
getCardImageUseCase.invoke(backup.card2.cardId, backup.card2.cardPublicKey)
_uiState.update {
it.copy(artwork2Url = artwork)
}
}
_uiState.value.artwork3Url == null && bst.card3 != null -> {
_uiState.value.artwork3Url == null && backup.card3 != null -> {
val artwork =
getCardImageUseCase.invoke(bst.card3.cardId, bst.card3.cardPublicKey)
getCardImageUseCase.invoke(backup.card3.cardId, backup.card3.cardPublicKey)
_uiState.update {
it.copy(artwork3Url = artwork)
}
@ -85,9 +104,10 @@ internal class OnboardingMultiWalletModel @Inject constructor(
private fun getInitialStep(): OnboardingMultiWalletState.Step {
val card = params.scanResponse.card
val backupService = backupServiceHolder.backupService.get()!!
// todo check local storage for scan response
return when {
backupService.hasIncompletedBackup -> OnboardingMultiWalletState.Step.Finalize
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup ->
OnboardingMultiWalletState.Step.AddBackupDevice
card.wallets.isNotEmpty() && card.backupStatus?.isActive == true ->
@ -97,6 +117,19 @@ internal class OnboardingMultiWalletModel @Inject constructor(
}
}
private fun getInitialStartFromFinalize(): FinalizeStage? {
val backupService = backupServiceHolder.backupService.get() ?: return null
return when (val state = backupService.currentState) {
BackupService.State.FinalizingPrimaryCard -> FinalizeStage.ScanPrimaryCard
is BackupService.State.FinalizingBackupCard -> if (state.index == 1) {
FinalizeStage.ScanBackupFirstCard
} else {
FinalizeStage.ScanBackupSecondCard
}
else -> null
}
}
private fun initScreenTitle() {
val title = screenTitleByStep(getInitialStep())
params.titleProvider.changeTitle(title)

View file

@ -8,8 +8,13 @@ data class OnboardingMultiWalletState(
val accessCode: AccessCode?,
val isThreeCards: Boolean,
val currentScanResponse: ScanResponse,
val resultUserWallet: UserWallet?, // TODO make callback instead
val startFromFinalize: FinalizeStage?,
val resultUserWallet: UserWallet?,
) {
enum class FinalizeStage {
ScanPrimaryCard, ScanBackupFirstCard, ScanBackupSecondCard
}
/**
* Wallet1
* CreateWallet -> ChooseBackupOption -> AddBackupDevice -> Finalize -> [Done]