From 80d661ab55989f58ef99f73b9560d41f897f4af7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Mar 2025 12:28:17 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../domain/scanCard/LegacyScanProcessor.kt | 16 +- .../chains/CheckForOnboardingChain.kt | 20 +- .../features/onboarding/OnboardingHelper.kt | 2 +- .../tangem/tap/routing/utils/ChildFactory.kt | 18 +- .../com/tangem/common/routing/AppRoute.kt | 4 +- .../v2/entry/OnboardingEntryComponent.kt | 8 +- features/onboarding-v2/impl/build.gradle.kts | 1 + .../v2/common/analytics/OnboardingEvent.kt | 18 + .../onboarding/v2/common/ui/Dialogs.kt | 16 +- .../impl/DefaultOnboardingEntryComponent.kt | 1 + .../entry/impl/model/OnboardingEntryModel.kt | 117 +++-- .../impl/routing/OnboardingChildFactory.kt | 20 +- .../v2/entry/impl/routing/OnboardingRoute.kt | 7 + .../v2/multiwallet/impl/common/ui/Dialogs.kt | 18 +- .../impl/model/OnboardingMultiWalletModel.kt | 27 +- .../impl/ui/OnboardingMultiWallet.kt | 20 - .../impl/ui/state/OnboardingMultiWalletUM.kt | 3 - .../v2/twin/api/OnboardingTwinComponent.kt | 5 +- .../impl/DefaultOnboardingTwinComponent.kt | 11 +- .../onboarding/v2/twin/impl/model/Dialogs.kt | 34 ++ .../v2/twin/impl/model/OnboardingTwinModel.kt | 419 +++++++++++++++++- .../v2/twin/impl/ui/OnboardingTwin.kt | 12 +- .../v2/twin/impl/ui/TwinWalletArtwork.kt | 43 +- .../v2/twin/impl/ui/state/OnboardingTwinUM.kt | 65 +++ .../model/WalletSettingsModel.kt | 2 +- .../router/DefaultWalletRouter.kt | 2 +- 26 files changed, 771 insertions(+), 138 deletions(-) create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/Dialogs.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index 27fa8ead9e..6ea8247faf 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -235,13 +235,25 @@ internal class LegacyScanProcessor @Inject constructor( } else { Analytics.setContext(scanResponse) + val isTwinRefactoringEnabled = + store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled + val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { onWalletNotCreated() - store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse))) - navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) } + if (isTwinRefactoringEnabled) { + navigateTo( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, + ), + ) { onProgressStateChange(it) } + } else { + store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse))) + navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) } + } } else { delay(DELAY_SDK_DIALOG_CLOSE) onSuccess(scanResponse) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt index 7bd5572200..b9d6aff5fd 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -59,15 +59,27 @@ class CheckForOnboardingChain( else -> { Analytics.setContext(previousChainResult) + val isTwinRefactoringEnabled = + store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled + val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase) .invokeSync() // If twins was twinned previously but twins welcome not shown if (previousChainResult.twinsIsTwinned() && !wasTwinsOnboardingShown) { - store.dispatchOnMain( - TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)), - ) - ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left() + if (isTwinRefactoringEnabled) { + ScanChainException.OnboardingNeeded( + AppRoute.Onboarding( + scanResponse = previousChainResult, + mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, + ), + ).left() + } else { + store.dispatchOnMain( + TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)), + ) + ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left() + } } else { delay(DELAY_SDK_DIALOG_CLOSE) previousChainResult.right() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 4d81a04308..7eda3eb6c5 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -85,7 +85,7 @@ object OnboardingHelper { scanResponse = scanResponse, mode = when (backupState.startedSource) { BackupStartedSource.Onboarding -> AppRoute.Onboarding.Mode.Onboarding - BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackup + BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackupWallet1 }, ) } diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index d4cd6dd17c..31a4ba6b7a 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -210,9 +210,12 @@ internal class ChildFactory @Inject constructor( contextProvider = contextProvider(route, contextFactory), params = OnboardingEntryComponent.Params( scanResponse = route.scanResponse, - multiWalletMode = when (route.mode) { - AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding - AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup + mode = when (route.mode) { + AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.Mode.Onboarding + AppRoute.Onboarding.Mode.AddBackupWallet1 -> OnboardingEntryComponent.Mode.AddBackupWallet1 + AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin + AppRoute.Onboarding.Mode.RecreateWalletTwin -> + OnboardingEntryComponent.Mode.RecreateWalletTwin }, ), componentFactory = onboardingEntryComponentFactory, @@ -702,9 +705,12 @@ internal class ChildFactory @Inject constructor( contextProvider = contextProvider(route, contextFactory), params = OnboardingEntryComponent.Params( scanResponse = route.scanResponse, - multiWalletMode = when (route.mode) { - AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding - AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup + mode = when (route.mode) { + AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.Mode.Onboarding + AppRoute.Onboarding.Mode.AddBackupWallet1 -> OnboardingEntryComponent.Mode.AddBackupWallet1 + AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin + AppRoute.Onboarding.Mode.RecreateWalletTwin -> + OnboardingEntryComponent.Mode.RecreateWalletTwin }, ), componentFactory = onboardingEntryComponentFactory, diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index f7e8d9ea7b..1238ad4305 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -260,7 +260,9 @@ sealed class AppRoute(val path: String) : Route { enum class Mode { Onboarding, // general Mode - AddBackup, // continue backup process for existing wallet 1 + AddBackupWallet1, // continue backup process for existing wallet 1 + WelcomeOnlyTwin, // show welcome screen and then navigate to wallet for twins + RecreateWalletTwin, // reset twins } } diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt index 9649b1e0c7..e591e8393f 100644 --- a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt @@ -8,12 +8,14 @@ interface OnboardingEntryComponent : ComposableContentComponent { data class Params( val scanResponse: ScanResponse, - val multiWalletMode: MultiWalletMode, + val mode: Mode, ) - enum class MultiWalletMode { + enum class Mode { Onboarding, - AddBackup, + AddBackupWallet1, + WelcomeOnlyTwin, + RecreateWalletTwin, } interface Factory : ComponentFactory diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index 3c11edf62d..f0b9fcacbb 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -77,6 +77,7 @@ dependencies { implementation(deps.kotlin.serialization) implementation(deps.timber) implementation(deps.firebase.crashlytics) + implementation(tangemDeps.card.core) /** DI */ implementation(deps.hilt.android) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt index 185b058558..053c5f9930 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt @@ -92,4 +92,22 @@ sealed class OnboardingEvent( params = mapOf("Action" to "Cancel"), ) } + + sealed class Twins( + event: String, + params: Map = mapOf(), + ) : OnboardingEvent("Onboarding / Twins", event, params) { + + data object ScreenOpened : Twins("Twinning Screen Opened") + data object SetupStarted : Twins("Twin Setup Started") + data object SetupFinished : Twins("Twin Setup Finished") + } + + data class OfflineAttestationFailed( + val source: AnalyticsParam.ScreensSources, + ) : OnboardingEvent( + category = "Error", + event = "Offline Attestation Failed", + params = mapOf(AnalyticsParam.SOURCE to source.value), + ) } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt index 2d26b8608b..7fb5ae3b9b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt @@ -22,4 +22,18 @@ internal fun exitOnboardingDialog(onConfirm: () -> Unit): DialogMessage { ) }, ) -} \ No newline at end of file +} + +internal fun interruptBackupDialog(onConfirm: () -> Unit) = DialogMessage( + title = resourceReference(R.string.onboarding_exit_alert_title), + message = resourceReference(R.string.onboarding_exit_alert_message), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_ok), + warning = true, + onClick = onConfirm, + ), + secondAction = EventMessageAction( + title = resourceReference(R.string.common_cancel), + onClick = {}, + ), +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt index 8e6cf0d6c5..ae53ea07e1 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt @@ -71,6 +71,7 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor( route = configuration, childContext = childByContext(factoryContext), onManageTokensDone = model::onManageTokensDone, + model = model, ) }, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index f8baf38360..231ffff139 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -15,6 +15,8 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.BiometryFeatureToggles @@ -22,9 +24,11 @@ import com.tangem.features.onboarding.v2.TitleProvider import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent +import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent.Mode import com.tangem.features.onboarding.v2.entry.impl.analytics.OnboardingEntryEvent import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.NonCancellable @@ -44,6 +48,7 @@ internal class OnboardingEntryModel @Inject constructor( private val askBiometryFeatureToggles: BiometryFeatureToggles, private val analyticsEventHandler: AnalyticsEventHandler, private val uiMessageSender: UiMessageSender, + private val userWalletsListManager: UserWalletsListManager, ) : Model() { private val params = paramsContainer.require() @@ -57,6 +62,7 @@ internal class OnboardingEntryModel @Inject constructor( } val startRoute = routeByProductType(params.scanResponse) + val onboardingTwinModelCallbacks = OnboardingTwinModelCallbacks() fun onManageTokensDone() { navigateToFinalScreenFlow() @@ -67,28 +73,25 @@ internal class OnboardingEntryModel @Inject constructor( } private fun routeByProductType(scanResponse: ScanResponse): OnboardingRoute { - val multiWalletNavigationMode = when (params.multiWalletMode) { - OnboardingEntryComponent.MultiWalletMode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding - OnboardingEntryComponent.MultiWalletMode.AddBackup -> OnboardingMultiWalletComponent.Mode.AddBackup - } - return when (scanResponse.productType) { - ProductType.Wallet -> OnboardingRoute.MultiWallet( - scanResponse = scanResponse, - withSeedPhraseFlow = false, - titleProvider = titleProvider, - onDone = ::onMultiWalletOnboardingDone, - mode = multiWalletNavigationMode, - ) - ProductType.Ring, + ProductType.Wallet, ProductType.Wallet2, - -> OnboardingRoute.MultiWallet( - scanResponse = scanResponse, - withSeedPhraseFlow = true, - titleProvider = titleProvider, - onDone = ::onMultiWalletOnboardingDone, - mode = multiWalletNavigationMode, - ) + ProductType.Ring, + -> { + val multiWalletNavigationMode = when (params.mode) { + Mode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding + Mode.AddBackupWallet1 -> OnboardingMultiWalletComponent.Mode.AddBackup + else -> error("Incorrect onboarding type") + } + + OnboardingRoute.MultiWallet( + scanResponse = scanResponse, + withSeedPhraseFlow = scanResponse.productType != ProductType.Wallet, + titleProvider = titleProvider, + onDone = ::onMultiWalletOnboardingDone, + mode = multiWalletNavigationMode, + ) + } ProductType.Visa -> OnboardingRoute.Visa( scanResponse = scanResponse, titleProvider = titleProvider, @@ -99,17 +102,31 @@ internal class OnboardingEntryModel @Inject constructor( titleProvider = titleProvider, onDone = ::navigateToFinalScreenFlow, ) + ProductType.Twins -> { + val mode = when (params.mode) { + Mode.Onboarding -> OnboardingTwinComponent.Params.Mode.CreateWallet + Mode.WelcomeOnlyTwin -> OnboardingTwinComponent.Params.Mode.WelcomeOnly + Mode.RecreateWalletTwin -> OnboardingTwinComponent.Params.Mode.RecreateWallet + else -> error("Incorrect onboarding type") + } + + OnboardingRoute.Twins( + scanResponse = scanResponse, + titleProvider = titleProvider, + mode = mode, + ) + } else -> error("Unsupported") } } private fun onMultiWalletOnboardingDone(userWallet: UserWallet) { when { - params.multiWalletMode == OnboardingEntryComponent.MultiWalletMode.AddBackup -> { + params.mode == Mode.AddBackupWallet1 -> { stackNavigation.replaceAll( OnboardingRoute.Done( mode = OnboardingDoneComponent.Mode.WalletCreated, - onDone = ::navigateToWalletScreen, + onDone = ::exitComponentScreen, ), ) } @@ -139,7 +156,7 @@ internal class OnboardingEntryModel @Inject constructor( stackNavigation.replaceAll( OnboardingRoute.Done( mode = doneMode, - onDone = ::navigateToWalletScreen, + onDone = ::exitComponentScreen, ), ) } @@ -148,7 +165,7 @@ internal class OnboardingEntryModel @Inject constructor( stackNavigation.replaceAll( OnboardingRoute.Done( mode = doneMode, - onDone = ::navigateToWalletScreen, + onDone = ::exitComponentScreen, ), ) } @@ -162,7 +179,7 @@ internal class OnboardingEntryModel @Inject constructor( stackNavigation.replaceAll( OnboardingRoute.Done( mode = doneMode, - onDone = ::navigateToWalletScreen, + onDone = ::exitComponentScreen, ), ) } @@ -172,23 +189,55 @@ internal class OnboardingEntryModel @Inject constructor( stackNavigation.replaceAll( OnboardingRoute.Done( mode = doneMode, - onDone = ::navigateToWalletScreen, + onDone = ::exitComponentScreen, ), ) } } - private fun navigateToWalletScreen() { + private fun exitComponentScreen() { if (askBiometryFeatureToggles.isAskForBiometryEnabled) { - router.replaceAll(AppRoute.Wallet) - } else { - modelScope.launch(NonCancellable) { - router.replaceAll(AppRoute.Wallet) - if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) { - delay(timeMillis = 1_800) - router.push(AppRoute.SaveWallet) + if (userWalletsListManager.hasUserWallets) { + val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false } + + if (isLocked) { + router.replaceAll(AppRoute.Welcome()) + } else { + router.replaceAll(AppRoute.Wallet) } + } else { + router.replaceAll(AppRoute.Home) + } + } else { + if (userWalletsListManager.hasUserWallets) { + val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false } + + if (isLocked) { + router.replaceAll(AppRoute.Welcome()) + } else { + modelScope.launch(NonCancellable) { + router.replaceAll(AppRoute.Wallet) + if (tangemSdkManager.checkCanUseBiometry() && + settingsRepository.shouldShowSaveUserWalletScreen() + ) { + delay(timeMillis = 1_800) + router.push(AppRoute.SaveWallet) + } + } + } + } else { + router.replaceAll(AppRoute.Home) } } } + + inner class OnboardingTwinModelCallbacks : OnboardingTwinComponent.ModelCallbacks { + override fun onDone() { + navigateToFinalScreenFlow() + } + + override fun onBack() { + router.pop() + } + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt index edbc555782..d0e8a13f03 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt @@ -4,11 +4,14 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.managetokens.component.OnboardingManageTokensComponent import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent +import com.tangem.features.onboarding.v2.entry.impl.model.OnboardingEntryModel import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent import javax.inject.Inject +@Suppress("LongParameterList") internal class OnboardingChildFactory @Inject constructor( private val multiWalletComponentFactory: OnboardingMultiWalletComponent.Factory, private val onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory, @@ -16,9 +19,15 @@ internal class OnboardingChildFactory @Inject constructor( private val onBoardingVisaComponentFactory: OnboardingVisaComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val onboardingNoteComponentFactory: OnboardingNoteComponent.Factory, + private val onboardingTwinComponentFactory: OnboardingTwinComponent.Factory, ) { - fun createChild(route: OnboardingRoute, childContext: AppComponentContext, onManageTokensDone: () -> Unit): Any { + fun createChild( + route: OnboardingRoute, + childContext: AppComponentContext, + model: OnboardingEntryModel, + onManageTokensDone: () -> Unit, + ): Any { return when (route) { is OnboardingRoute.MultiWallet -> multiWalletComponentFactory.create( context = childContext, @@ -60,6 +69,15 @@ internal class OnboardingChildFactory @Inject constructor( onDone = route.onDone, ), ) + is OnboardingRoute.Twins -> onboardingTwinComponentFactory.create( + context = childContext, + params = OnboardingTwinComponent.Params( + scanResponse = route.scanResponse, + modelCallbacks = model.onboardingTwinModelCallbacks, + titleProvider = route.titleProvider, + mode = route.mode, + ), + ) is OnboardingRoute.AskBiometry -> askBiometryComponentFactory.create( context = childContext, params = AskBiometryComponent.Params( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt index 1175f2948b..4d0b03129a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt @@ -7,6 +7,7 @@ import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.onboarding.v2.TitleProvider import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent sealed class OnboardingRoute : Route { @@ -32,6 +33,12 @@ sealed class OnboardingRoute : Route { val onDone: () -> Unit, ) : OnboardingRoute() + data class Twins( + val titleProvider: TitleProvider, + val scanResponse: ScanResponse, + val mode: OnboardingTwinComponent.Params.Mode, + ) : OnboardingRoute() + data class ManageTokens( val userWallet: UserWallet, ) : OnboardingRoute() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt index 771c7b9085..9b1a7be5b1 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt @@ -20,20 +20,4 @@ internal fun resetCardDialog(onConfirm: () -> Unit, dismiss: () -> Unit, onDismi onDismissButtonClick() dismiss() }, - ) - -internal fun interruptBackupDialog(onConfirm: () -> Unit, dismiss: () -> Unit) = OnboardingDialogUM( - title = resourceReference(R.string.onboarding_exit_alert_title), - message = resourceReference(R.string.onboarding_exit_alert_message), - confirmButtonText = resourceReference(R.string.common_ok), - dismissButtonText = resourceReference(R.string.common_cancel), - dismissWarningColor = true, - onDismiss = dismiss, - onConfirmClick = { - onConfirm() - dismiss() - }, - onDismissButtonClick = { - dismiss() - }, -) \ No newline at end of file + ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt index f868e3915a..c55cb1d1a9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt @@ -5,14 +5,15 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog 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 @@ -35,6 +36,7 @@ internal class OnboardingMultiWalletModel @Inject constructor( private val backupServiceHolder: BackupServiceHolder, private val onboardingRepository: OnboardingRepository, private val getCardImageUseCase: GetCardImageUseCase, + private val uiMessageSender: UiMessageSender, ) : Model() { private val params = paramsContainer.require() private val _uiState = MutableStateFlow(OnboardingMultiWalletUM()) @@ -60,19 +62,16 @@ internal class OnboardingMultiWalletModel @Inject constructor( } fun onBack() { - _uiState.update { st -> - st.copy( - dialog = interruptBackupDialog( - onConfirm = { - modelScope.launch { - onboardingRepository.clearUnfinishedFinalizeOnboarding() - router.pop() - } - }, - dismiss = { _uiState.update { it.copy(dialog = null) } }, - ), - ) - } + uiMessageSender.send( + interruptBackupDialog( + onConfirm = { + modelScope.launch { + onboardingRepository.clearUnfinishedFinalizeOnboarding() + router.pop() + } + }, + ), + ) } private fun subscribeToBackups() { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt index 62bc6aaf7a..4af97756ab 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt @@ -8,9 +8,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color 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.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM @@ -23,23 +20,6 @@ internal fun OnboardingMultiWallet( modifier: Modifier = Modifier, childContent: @Composable (Modifier) -> Unit = {}, ) { - 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(), ) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt index 74f7cc7bec..829828761d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt @@ -1,10 +1,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.ui.state -import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM - internal data class OnboardingMultiWalletUM( val artwork1Url: String? = null, val artwork2Url: String? = null, val artwork3Url: String? = null, - val dialog: OnboardingDialogUM? = null, ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt index 20893e09f8..2a28cf2e89 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt @@ -8,18 +8,19 @@ import com.tangem.features.onboarding.v2.TitleProvider interface OnboardingTwinComponent : ComposableContentComponent { data class Params( - val titleProvider: TitleProvider, val scanResponse: ScanResponse, val modelCallbacks: ModelCallbacks, + val titleProvider: TitleProvider, val mode: Mode, ) { enum class Mode { - CreateWallet, RecreateWallet + WelcomeOnly, CreateWallet, RecreateWallet } } interface ModelCallbacks { fun onDone() + fun onBack() } interface Factory : ComponentFactory diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt index 318143250b..f8130f2d71 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt @@ -1,9 +1,12 @@ package com.tangem.features.onboarding.v2.twin.impl +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier 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.twin.api.OnboardingTwinComponent import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel import dagger.assisted.Assisted @@ -17,9 +20,15 @@ internal class DefaultOnboardingTwinComponent @AssistedInject constructor( private val model: OnboardingTwinModel = getOrCreateModel(params) + init { + params.titleProvider.changeTitle(resourceReference(R.string.twins_recreate_toolbar)) + } + @Composable override fun Content(modifier: Modifier) { - TODO("Not yet implemented") + BackHandler { model.onBack() } + + // TODO } @AssistedFactory diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/Dialogs.kt new file mode 100644 index 0000000000..a819a60957 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/Dialogs.kt @@ -0,0 +1,34 @@ +package com.tangem.features.onboarding.v2.twin.impl.model + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.onboarding.v2.impl.R + +internal val TwinProcessNotCompleted = DialogMessage( + message = resourceReference(R.string.onboarding_twin_exit_warning), + firstAction = EventMessageAction( + title = resourceReference(R.string.warning_button_ok), + onClick = {}, + ), +) + +internal fun cardVerificationFailedDialog( + errorDescription: TextReference, + onRequestSupport: () -> Unit, + onCancelClick: () -> Unit = {}, +): DialogMessage { + return DialogMessage( + message = errorDescription, + title = resourceReference(id = R.string.security_alert_title), + isDismissable = false, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(id = R.string.alert_button_request_support), + onClick = onRequestSupport, + ) + }, + secondActionBuilder = { cancelAction(onClick = onCancelClick) }, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 047e897976..ae6846a3ec 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -1,19 +1,434 @@ package com.tangem.features.onboarding.v2.twin.impl.model +import com.tangem.Message +import com.tangem.common.CompletionResult +import com.tangem.common.KeyPair +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.hexToBytes +import com.tangem.common.extensions.toHexString +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.toWrappedList +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.datasource.local.config.issuers.IssuersConfigStorage +import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.common.TwinCardNumber +import com.tangem.domain.common.getTwinCardNumber +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase +import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog +import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent.Params.Mode +import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM +import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM +import com.tangem.sdk.api.TangemSdkManager +import com.tangem.sdk.extensions.localizedDescriptionRes import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList", "LargeClass") @ModelScoped internal class OnboardingTwinModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, + private val userWalletsListManager: UserWalletsListManager, + private val analyticsEventHandler: AnalyticsEventHandler, + private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase, + private val tangemSdkManager: TangemSdkManager, + private val issuersConfigStorage: IssuersConfigStorage, + private val cardRepository: CardRepository, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, + private val urlOpener: UrlOpener, + private val uiMessageSender: UiMessageSender, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, ) : Model() { - val params = paramsContainer.require() + private val params = paramsContainer.require() + private val firstCardTwinNumber = params.scanResponse.card.getTwinCardNumber() ?: error("Not twin") + private val cryptoCurrencyStatusJobHolder = JobHolder() - // TODO + private val _uiState = MutableStateFlow( + when (params.mode) { + Mode.WelcomeOnly -> { + OnboardingTwinUM.Welcome(onContinueClick = ::saveWalletAndDone) + } + Mode.RecreateWallet -> { + OnboardingTwinUM.ResetWarning( + onAcceptClick = { toggle -> + update { it.copy(acceptToggle = toggle) } + }, + onContinueClick = { + navigateToFirstScan() + }, + ) + } + Mode.CreateWallet -> { + OnboardingTwinUM.Welcome(onContinueClick = ::navigateToFirstScan) + } + }, + ) + + val uiState = _uiState.asStateFlow() + + init { + if (_uiState.value is OnboardingTwinUM.Welcome) { + analyticsEventHandler.send(OnboardingEvent.Twins.ScreenOpened) + modelScope.launch { + saveTwinsOnboardingShownUseCase() + } + } + } + + fun onBack() { + val state = _uiState.value + val twinningInProgress = state is OnboardingTwinUM.ScanCard && + (state.step == OnboardingTwinUM.ScanCard.Step.Second || state.step == OnboardingTwinUM.ScanCard.Step.Third) + + if (twinningInProgress) { + uiMessageSender.send(TwinProcessNotCompleted) + } else { + uiMessageSender.send( + interruptBackupDialog( + onConfirm = { params.modelCallbacks.onBack() }, + ), + ) + } + } + + private fun navigateToFirstScan() { + _uiState.value = OnboardingTwinUM.ScanCard( + artworkStep = when (firstCardTwinNumber) { + TwinCardNumber.First -> TwinWalletArtworkUM.Leapfrog.Step.FirstCard + TwinCardNumber.Second -> TwinWalletArtworkUM.Leapfrog.Step.SecondCard + }, + step = OnboardingTwinUM.ScanCard.Step.First, + onScanClick = { createFirstWallet() }, + ) + } + + private fun createFirstWallet() { + setLoading(true) + + modelScope.launch { + val result = tangemSdkManager.createFirstTwinWallet( + cardId = params.scanResponse.card.cardId, + initialMessage = Message( + tangemSdkManager.getString( + R.string.twins_recreate_title_format, + wrappedList(firstCardTwinNumber.number), + ), + ), + ) + + when (result) { + is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) + is CompletionResult.Success -> { + if (params.mode == Mode.CreateWallet) { + cardRepository.startCardActivation(result.data.cardId) + } + + // remove wallet only after first step of retwin + if (params.mode == Mode.RecreateWallet) { + userWalletsListManager.delete( + listOfNotNull(UserWalletIdBuilder.scanResponse(params.scanResponse).build()), + ) + } + + analyticsEventHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) + + update { + it.copy( + isLoading = false, + artworkStep = it.artworkStep.next(), + step = OnboardingTwinUM.ScanCard.Step.Second, + onScanClick = { + createSecondWallet(firstPublicKey = result.data.wallet.publicKey.toHexString()) + }, + ) + } + } + } + } + } + + private fun createSecondWallet(firstPublicKey: String) { + setLoading(true) + + modelScope.launch { + val secondCardNumber = firstCardTwinNumber.pairNumber().number + val result = tangemSdkManager.createSecondTwinWallet( + firstPublicKey = firstPublicKey, + firstCardId = params.scanResponse.card.cardId, + issuerKeys = getIssuerKeys(), + preparingMessage = Message( + tangemSdkManager.getString(R.string.twins_recreate_title_preparing), + ), + creatingWalletMessage = Message( + tangemSdkManager.getString(R.string.twins_recreate_title_creating_wallet), + ), + initialMessage = Message( + tangemSdkManager.getString( + R.string.twins_recreate_title_format, + wrappedList(secondCardNumber), + ), + ), + ) + + when (result) { + is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) + is CompletionResult.Success -> { + if (params.mode == Mode.CreateWallet) { + cardRepository.startCardActivation(result.data.cardId) + } + + update { + it.copy( + isLoading = false, + artworkStep = it.artworkStep.next(), + step = OnboardingTwinUM.ScanCard.Step.Third, + onScanClick = { + createThirdWallet( + secondCardPublicKey = result.data.wallet.publicKey, + ) + }, + ) + } + } + } + } + } + + private fun createThirdWallet(secondCardPublicKey: ByteArray) { + setLoading(true) + + modelScope.launch { + val result = tangemSdkManager.finalizeTwin( + secondCardPublicKey = secondCardPublicKey, + issuerKeyPair = getIssuerKeys(), + cardId = params.scanResponse.card.cardId, + initialMessage = Message( + tangemSdkManager.getString( + R.string.twins_recreate_title_format, + wrappedList(firstCardTwinNumber.number), + ), + ), + ) + + when (result) { + is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) + is CompletionResult.Success -> { + scanComplete(result.data) + } + } + } + } + + private fun scanComplete(scanResponse: ScanResponse) { + analyticsEventHandler.send(OnboardingEvent.Twins.SetupFinished) + + when (params.mode) { + Mode.WelcomeOnly -> saveWalletAndDone() + Mode.RecreateWallet -> params.modelCallbacks.onDone() + Mode.CreateWallet -> { + modelScope.launch { + setLoading(true) + + val userWallet = userWalletBuilderFactory.create(scanResponse).build() ?: run { + Timber.e("User wallet not created") + setLoading(false) + return@launch + } + + fetchCurrencyStatusUseCase( + userWalletId = userWallet.walletId, + refresh = true, + ) + + val cryptoCurrencyStatus = + getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId).firstOrNull()?.getOrNull() + ?: run { + setLoading(false) + return@launch + } + + launch { + getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId) + .collect { + it.onRight { status -> + applyCryptoCurrencyStatusToState(status) + } + } + } + + _uiState.value = OnboardingTwinUM.TopUp( + onBuyCryptoClick = { onBuyCryptoClick(cryptoCurrencyStatus) }, + onRefreshClick = { onRefreshBalanceClick(userWallet) }, + onShowAddressClick = { onShowAddressClick(cryptoCurrencyStatus) }, + isLoading = true, + ) + }.saveIn(cryptoCurrencyStatusJobHolder) + } + } + } + + private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { + update { + it.copy( + balance = (status.value as? CryptoCurrencyStatus.Loaded)?.amount + ?.format { crypto(status.currency) } ?: "", + onBuyCryptoClick = { onBuyCryptoClick(status) }, + onShowAddressClick = { onShowAddressClick(status) }, + isLoading = false, + ) + } + } + + private fun onBuyCryptoClick(status: CryptoCurrencyStatus) { + modelScope.launch { + getLegacyTopUpUrlUseCase(status).onRight { + urlOpener.openUrl(it) + } + } + } + + private fun onShowAddressClick(status: CryptoCurrencyStatus) { + val currency = status.currency + val networkAddress = status.value.networkAddress ?: return + + update { + it.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = { + update { + it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty) + } + }, + content = TokenReceiveBottomSheetConfig( + name = currency.name, + symbol = currency.symbol, + network = currency.network.name, + addresses = networkAddress.availableAddresses + .mapToAddressModels(currency) + .toImmutableList(), + showMemoDisclaimer = + currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, + onCopyClick = {}, + onShareClick = {}, + ), + ), + ) + } + } + + private fun onRefreshBalanceClick(userWallet: UserWallet) { + update { + it.copy(isLoading = true) + } + modelScope.launch { + fetchCurrencyStatusUseCase( + userWalletId = userWallet.walletId, + refresh = true, + ) + } + } + + private fun saveWalletAndDone() { + setLoading(true) + + modelScope.launch { + val userWallet = userWalletBuilderFactory.create(params.scanResponse).build() ?: run { + Timber.e("User wallet not created") + setLoading(false) + return@launch + } + + userWalletsListManager.save(userWallet, canOverride = true) + params.modelCallbacks.onDone() + } + } + + private fun showCardVerificationFailedDialog(error: TangemError) { + setLoading(false) + if (error !is TangemSdkError.CardVerificationFailed) return + + analyticsEventHandler.send(OnboardingEvent.OfflineAttestationFailed(AnalyticsParam.ScreensSources.Onboarding)) + + val resource = error.localizedDescriptionRes() + val resId = resource.resId ?: R.string.common_unknown_error + val resArgs = resource.args.map { it.value } + + uiMessageSender.send( + cardVerificationFailedDialog( + errorDescription = resourceReference(id = resId, resArgs.toWrappedList()), + onRequestSupport = { + modelScope.launch { + sendFeedbackEmailUseCase(type = FeedbackEmailType.CardAttestationFailed) + } + }, + ), + ) + } + + private suspend fun getIssuerKeys(): KeyPair { + val issuer = issuersConfigStorage.getConfig() + .first { it.publicKey == params.scanResponse.card.issuer.publicKey.toHexString() } + + return KeyPair( + publicKey = issuer.publicKey.hexToBytes(), + privateKey = issuer.privateKey.hexToBytes(), + ) + } + + private inline fun update(updateBlock: (T) -> OnboardingTwinUM) { + _uiState.update { st -> + (st as? T)?.let { updateBlock(it) } ?: st + } + } + + private fun setLoading(flag: Boolean) { + _uiState.update { it.copySealed(isLoading = flag) } + } + + private fun TwinWalletArtworkUM.Leapfrog.Step.next(): TwinWalletArtworkUM.Leapfrog.Step = when (this) { + TwinWalletArtworkUM.Leapfrog.Step.FirstCard -> TwinWalletArtworkUM.Leapfrog.Step.SecondCard + TwinWalletArtworkUM.Leapfrog.Step.SecondCard -> TwinWalletArtworkUM.Leapfrog.Step.FirstCard + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt index 2a8405c685..5ea7351de4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt @@ -5,22 +5,28 @@ import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM @Suppress("UnusedPrivateMember") @Composable -fun OnboardingTwin(modifier: Modifier = Modifier) { - Column { +internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifier) { + Column(modifier = modifier) { Box { // TODO } } + + if (state is OnboardingTwinUM.TopUp) { + TokenReceiveBottomSheet(config = state.bottomSheetConfig) + } } @Preview @Composable private fun Preview() { TangemThemePreview { - OnboardingTwin() + OnboardingTwin(OnboardingTwinUM.Welcome()) } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt index c2a89c2b69..2326dd841b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt @@ -33,18 +33,19 @@ import com.tangem.features.onboarding.v2.impl.R import kotlinx.coroutines.delay import java.util.concurrent.TimeUnit -internal sealed class TwinWalletArtworkState { - data object Spread : TwinWalletArtworkState() +@Immutable +internal sealed class TwinWalletArtworkUM { + data object Spread : TwinWalletArtworkUM() data class Leapfrog( val step: Step, - ) : TwinWalletArtworkState() { + ) : TwinWalletArtworkUM() { enum class Step { FirstCard, SecondCard } } - data object TopUp : TwinWalletArtworkState() + data object TopUp : TwinWalletArtworkUM() } private data class CardsTransitionState( @@ -65,7 +66,7 @@ private data class WalletCardTransitionState( @Suppress("LongMethod") @Composable internal fun TwinWalletArtworks( - state: TwinWalletArtworkState, + state: TwinWalletArtworkUM, balance: String, isRefreshing: Boolean, onRefreshBalanceClick: () -> Unit, @@ -109,7 +110,7 @@ internal fun TwinWalletArtworks( } AnimatedVisibility( - visible = state == TwinWalletArtworkState.TopUp, + visible = state == TwinWalletArtworkUM.TopUp, enter = fadeIn(), exit = fadeOut(), ) { @@ -134,7 +135,7 @@ internal fun TwinWalletArtworks( AnimatedVisibility( modifier = Modifier.align(Alignment.Center), - visible = state == TwinWalletArtworkState.TopUp, + visible = state == TwinWalletArtworkUM.TopUp, enter = fadeIn(), exit = fadeOut(), ) { @@ -151,7 +152,7 @@ internal fun TwinWalletArtworks( ) SpacerH8() Text( - text = balance.orEmpty(), + text = balance, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -162,7 +163,7 @@ internal fun TwinWalletArtworks( AnimatedVisibility( modifier = Modifier.align(Alignment.BottomCenter), - visible = state == TwinWalletArtworkState.TopUp, + visible = state == TwinWalletArtworkUM.TopUp, enter = fadeIn(), exit = fadeOut(), ) { @@ -234,12 +235,12 @@ private fun AnimatedTwinCards( } @Suppress("MagicNumber", "LongMethod", "UnnecessaryParentheses") -private fun TwinWalletArtworkState.toTransitionSetState( +private fun TwinWalletArtworkUM.toTransitionSetState( maxWidthDp: Float, maxHeightDp: Float, density: Float, ): List = when (this) { - TwinWalletArtworkState.Spread -> { + TwinWalletArtworkUM.Spread -> { val scale = 0.8f listOf( CardsTransitionState( @@ -262,7 +263,7 @@ private fun TwinWalletArtworkState.toTransitionSetState( ), ) } - is TwinWalletArtworkState.Leapfrog -> { + is TwinWalletArtworkUM.Leapfrog -> { val translationY = -(maxHeightDp / 2) * density val secondCardPlace = WalletCardTransitionState().copy( xTranslation = 0f, @@ -272,7 +273,7 @@ private fun TwinWalletArtworkState.toTransitionSetState( yScale = 0.83f, ) - if (step == TwinWalletArtworkState.Leapfrog.Step.FirstCard) { + if (step == TwinWalletArtworkUM.Leapfrog.Step.FirstCard) { listOf( CardsTransitionState( walletCard1 = WalletCardTransitionState( @@ -306,7 +307,7 @@ private fun TwinWalletArtworkState.toTransitionSetState( ) } } - TwinWalletArtworkState.TopUp -> { + TwinWalletArtworkUM.TopUp -> { val scale = 0.4f val yTranslation = -maxHeightDp * density - 24 * density listOf( @@ -339,7 +340,7 @@ private fun Preview() { .fillMaxSize(), contentAlignment = Alignment.Center, ) { - var state: TwinWalletArtworkState by remember { mutableStateOf(TwinWalletArtworkState.TopUp) } + var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.TopUp) } TwinWalletArtworks( state = state, @@ -359,12 +360,12 @@ private fun Preview() { .align(Alignment.TopStart), onClick = { val list = listOf( - TwinWalletArtworkState.Spread, - TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.FirstCard), - TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.SecondCard), - TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.FirstCard), - TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.SecondCard), - TwinWalletArtworkState.TopUp, + TwinWalletArtworkUM.Spread, + TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard), + TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), + TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard), + TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), + TwinWalletArtworkUM.TopUp, ) state = list[index % list.size] diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt new file mode 100644 index 0000000000..88bd97bcbc --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt @@ -0,0 +1,65 @@ +package com.tangem.features.onboarding.v2.twin.impl.ui.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM + +@Immutable +internal sealed class OnboardingTwinUM { + + abstract val stepIndex: Int + abstract val isLoading: Boolean + abstract val artwork: TwinWalletArtworkUM + + data class Welcome( + override val isLoading: Boolean = false, + val onContinueClick: () -> Unit = {}, + ) : OnboardingTwinUM() { + override val stepIndex = 0 + override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread + } + + data class ResetWarning( + val acceptToggle: Boolean = false, + val onAcceptClick: (Boolean) -> Unit = {}, + val onContinueClick: () -> Unit = {}, + ) : OnboardingTwinUM() { + override val stepIndex: Int = 0 + override val isLoading: Boolean = false + override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread + } + + data class ScanCard( + override val isLoading: Boolean = false, + val artworkStep: TwinWalletArtworkUM.Leapfrog.Step, + val step: Step = Step.First, + val onScanClick: () -> Unit = {}, + ) : OnboardingTwinUM() { + + enum class Step { + First, Second, Third + } + + override val stepIndex: Int = 1 + override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Leapfrog(artworkStep) + } + + data class TopUp( + override val isLoading: Boolean = false, + val balance: String = "", + val bottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, + val onBuyCryptoClick: () -> Unit = {}, + val onShowAddressClick: () -> Unit = {}, + val onRefreshClick: () -> Unit = {}, + ) : OnboardingTwinUM() { + override val stepIndex: Int = 2 + override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.TopUp + } + + fun copySealed(isLoading: Boolean = this.isLoading): OnboardingTwinUM = when (this) { + is Welcome -> copy(isLoading = isLoading) + is ResetWarning -> copy() + is ScanCard -> copy(isLoading = isLoading) + is TopUp -> copy(isLoading = isLoading) + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 52f23e9529..cc48296697 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -164,7 +164,7 @@ internal class WalletSettingsModel @Inject constructor( router.push( AppRoute.Onboarding( scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.AddBackup, + mode = AppRoute.Onboarding.Mode.AddBackupWallet1, ), ) } else { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 967c19ebed..05a688f302 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -54,7 +54,7 @@ internal class DefaultWalletRouter @Inject constructor( AppRoute.Onboarding( scanResponse = scanResponse, mode = if (continueBackup) { - AppRoute.Onboarding.Mode.AddBackup + AppRoute.Onboarding.Mode.AddBackupWallet1 } else { AppRoute.Onboarding.Mode.Onboarding },