Updated on 2026-08-14
This commit is contained in:
parent
80d00dfb6b
commit
80d661ab55
26 changed files with 771 additions and 138 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Params, OnboardingEntryComponent>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -92,4 +92,22 @@ sealed class OnboardingEvent(
|
|||
params = mapOf("Action" to "Cancel"),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Twins(
|
||||
event: String,
|
||||
params: Map<String, String> = 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),
|
||||
)
|
||||
}
|
||||
|
|
@ -22,4 +22,18 @@ internal fun exitOnboardingDialog(onConfirm: () -> Unit): DialogMessage {
|
|||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 = {},
|
||||
),
|
||||
)
|
||||
|
|
@ -71,6 +71,7 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
|
|||
route = configuration,
|
||||
childContext = childByContext(factoryContext),
|
||||
onManageTokensDone = model::onManageTokensDone,
|
||||
model = model,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<OnboardingEntryComponent.Params>()
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
)
|
||||
|
|
@ -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<OnboardingMultiWalletComponent.Params>()
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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<Params, OnboardingTwinComponent>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
|
|
@ -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<OnboardingTwinComponent.Params>()
|
||||
private val params = paramsContainer.require<OnboardingTwinComponent.Params>()
|
||||
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<OnboardingTwinUM.ResetWarning> { 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<OnboardingTwinUM.ScanCard> {
|
||||
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<OnboardingTwinUM.ScanCard> {
|
||||
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<OnboardingTwinUM.TopUp> {
|
||||
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<OnboardingTwinUM.TopUp> {
|
||||
it.copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {
|
||||
update<OnboardingTwinUM.TopUp> {
|
||||
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<OnboardingTwinUM.TopUp> {
|
||||
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 <reified T : OnboardingTwinUM> 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CardsTransitionState> = 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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue