Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-25 13:04:54 +03:00
parent cf05b371ef
commit e080401183
103 changed files with 142 additions and 10555 deletions

View file

@ -158,7 +158,6 @@ dependencies {
implementation(projects.data.blockaid)
/** Features */
implementation(projects.features.onboarding)
implementation(projects.features.referral.impl)
implementation(projects.features.referral.domain)
implementation(projects.features.referral.data)

View file

@ -62,13 +62,11 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.google.GoogleServicesHelper
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.extensions.init
import com.tangem.tap.common.ActivityResultCallbackHolder
import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.OnActivityResultCallback
@ -83,7 +81,6 @@ import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandle
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.routing.component.RoutingComponent
@ -181,9 +178,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var backupServiceHolder: BackupServiceHolder
@Inject
lateinit var onboardingV2FeatureToggles: OnboardingV2FeatureToggles
@Inject
lateinit var setGoogleServicesAvailabilityUseCase: SetGoogleServicesAvailabilityUseCase
@ -364,12 +358,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
cardSdkOwner.register(activity = this)
tangemSdkManager = injectedTangemSdkManager
if (onboardingV2FeatureToggles.isOnboardingV2Enabled) {
backupServiceHolder.createAndSetService(cardSdkConfigRepository.sdk, this)
backupService = backupServiceHolder.backupService.get()!! // will be deleted eventually
} else {
backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
}
backupServiceHolder.createAndSetService(cardSdkConfigRepository.sdk, this)
backupService = backupServiceHolder.backupService.get()!! // will be deleted eventually
lockUserWalletsTimer = LockUserWalletsTimer(
context = this,
@ -662,7 +652,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
store.dispatch(BackupAction.CheckForUnfinishedBackup)
viewModel.checkForUnfinishedBackup()
}
private fun observePolkadotAccountHealthCheck() {

View file

@ -9,9 +9,11 @@ import com.tangem.tap.common.ui.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.products.twins.ui.dialog.TwinningProcessNotCompletedDialog
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.*
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletActivationErrorDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletAlreadyWasUsedDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@ -58,8 +60,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
is OnboardingDialog.TwinningProcessNotCompleted -> TwinningProcessNotCompletedDialog.create(context)
is OnboardingDialog.InterruptOnboarding -> InterruptOnboardingDialog.create(context, state.dialog)
is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(
@ -143,9 +143,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
messageRes = R.string.unsupported_wc_version,
context = context,
)
is BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context)
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(
context = context,
scanResponse = state.dialog.scanResponse,
@ -154,10 +151,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
context = context,
unfinishedBackupScanResponse = state.dialog.scanResponse,
)
is BackupDialog.ResetBackupCard -> ResetBackupCardDialog.create(
context = context,
cardId = state.dialog.cardId,
)
is AppDialog.TokensAreLinkedDialog -> SimpleAlertDialog.create(
title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol),
message = context.getString(

View file

@ -4,10 +4,6 @@ import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
import com.tangem.tap.features.home.redux.HomeReducer
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.redux.DaggerGraphReducer
@ -20,10 +16,6 @@ fun appReducer(action: Action, state: AppState?): AppState {
return AppState(
globalState = globalReducer(action, state),
homeState = HomeReducer.reduce(action, state),
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),
onboardingWalletState = OnboardingWalletReducer.reduce(action, state),
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
twinCardsState = TwinCardsReducer.reduce(action, state),
detailsState = DetailsReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
welcomeState = WelcomeReducer.reduce(action, state),

View file

@ -9,15 +9,7 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddlewa
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.home.redux.HomeMiddleware
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteMiddleware
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsMiddleware
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsState
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsMiddleware
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
@ -31,10 +23,6 @@ import org.rekotlin.StateType
data class AppState(
val globalState: GlobalState = GlobalState(),
val homeState: HomeState = HomeState(),
val onboardingNoteState: OnboardingNoteState = OnboardingNoteState(),
val onboardingWalletState: OnboardingWalletState = OnboardingWalletState(),
val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(),
val twinCardsState: TwinCardsState = TwinCardsState(),
val detailsState: DetailsState = DetailsState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val welcomeState: WelcomeState = WelcomeState(),
@ -49,10 +37,6 @@ data class AppState(
notificationsMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
OnboardingNoteMiddleware.handler,
OnboardingWalletMiddleware.handler,
OnboardingOtherCardsMiddleware.handler,
TwinCardsMiddleware.handler,
DetailsMiddleware().detailsMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,

View file

@ -8,7 +8,6 @@ import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.redux.DebugErrorAction
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import org.rekotlin.Action
sealed class GlobalAction : Action {
@ -21,29 +20,6 @@ sealed class GlobalAction : Action {
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()
sealed class Onboarding : GlobalAction() {
/**
* Initiate an onboarding process.
* For resuming unfinished backup of standard Wallet see
* BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup
*/
data class Start(
val scanResponse: ScanResponse,
val source: BackupStartedSource,
val canSkipBackup: Boolean = true,
) : Onboarding()
/**
* Initiate resuming of unfinished backup for standard Wallet.
* See more BackupAction.CheckForUnfinishedBackup
*/
data class StartForUnfinishedBackup(val addedBackupCardsCount: Int) : Onboarding()
object Stop : Onboarding()
data class ShouldResetCardOnCreate(val shouldReset: Boolean) : Onboarding()
}
object ScanFailsCounter {
data class ChooseBehavior(
val result: CompletionResult<ScanResponse>,

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.redux.global
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.utils.extensions.replaceBy
import org.rekotlin.Action
@ -12,21 +11,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
val globalState = state.globalState
return when (action) {
is GlobalAction.Onboarding.Start -> {
val onboardingManager = OnboardingManager(action.scanResponse)
globalState.copy(onboardingState = OnboardingState(true, onboardingManager))
}
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
globalState.copy(onboardingState = OnboardingState(true, null))
}
is GlobalAction.Onboarding.Stop -> {
globalState.copy(onboardingState = OnboardingState(false))
}
is GlobalAction.Onboarding.ShouldResetCardOnCreate -> {
globalState.copy(
onboardingState = globalState.onboardingState.copy(shouldResetOnCreate = action.shouldReset),
)
}
is GlobalAction.ScanFailsCounter.Increment -> {
globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1)
}

View file

@ -5,10 +5,8 @@ import com.tangem.domain.redux.LegacyAction
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.AppSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
@ -26,15 +24,6 @@ internal object LegacyMiddleware {
{ next ->
{ action ->
when (action) {
is LegacyAction.StartOnboardingProcess -> {
store.dispatch(
GlobalAction.Onboarding.Start(
scanResponse = action.scanResponse,
source = BackupStartedSource.CreateBackup,
canSkipBackup = action.canSkipBackup,
),
)
}
is LegacyAction.PrepareDetailsScreen -> {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)

View file

@ -1,24 +0,0 @@
package com.tangem.tap.di.domain
import android.content.Context
import com.tangem.crypto.bip39.Wordlist
import com.tangem.feature.onboarding.data.DefaultMnemonicRepository
import com.tangem.feature.onboarding.data.MnemonicRepository
import com.tangem.sdk.extensions.getWordlist
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class MnemonicModule {
@Provides
@Singleton
fun provideMnemonicRepository(@ApplicationContext context: Context): MnemonicRepository {
return DefaultMnemonicRepository(Wordlist.getWordlist(context))
}
}

View file

@ -7,7 +7,6 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -42,7 +41,6 @@ class TapWalletManager(
withMainContext {
// Order is important
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
}
}

View file

@ -21,7 +21,6 @@ import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.feedback.models.FeedbackEmailType
@ -34,9 +33,6 @@ import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
@ -208,8 +204,6 @@ internal class LegacyScanProcessor @Inject constructor(
crossinline onCancel: suspend () -> Unit,
crossinline onSuccess: suspend (ScanResponse) -> Unit,
) {
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse))
checkCardWasUsedInApp(
scanResponse = scanResponse,
onCancel = {
@ -222,38 +216,26 @@ internal class LegacyScanProcessor @Inject constructor(
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
Analytics.addContext(scanResponse)
onWalletNotCreated()
// must check skip backup using card canSkipBackup
store.dispatchOnMain(
GlobalAction.Onboarding.Start(
navigateTo(
AppRoute.Onboarding(
scanResponse = scanResponse,
source = BackupStartedSource.Onboarding,
canSkipBackup = scanResponse.card.canSkipBackup,
mode = AppRoute.Onboarding.Mode.Onboarding,
),
)
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
navigateTo(appScreen) { onProgressStateChange(it) }
) { onProgressStateChange(it) }
} else {
Analytics.setContext(scanResponse)
val isTwinRefactoringEnabled =
store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled
val wasTwinsOnboardingShown =
store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync()
if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) {
onWalletNotCreated()
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) }
}
navigateTo(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin,
),
) { onProgressStateChange(it) }
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)

View file

@ -5,21 +5,15 @@ import arrow.core.right
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
@ -40,46 +34,30 @@ class CheckForOnboardingChain(
) : ResultChain<ScanCardException, ScanResponse>() {
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult))
return when {
OnboardingHelper.isOnboardingCase(previousChainResult) -> {
Analytics.addContext(previousChainResult)
// must check skip backup using card canSkipBackup
store.dispatchOnMain(
GlobalAction.Onboarding.Start(
ScanChainException.OnboardingNeeded(
AppRoute.Onboarding(
scanResponse = previousChainResult,
source = BackupStartedSource.Onboarding,
canSkipBackup = previousChainResult.card.canSkipBackup,
mode = AppRoute.Onboarding.Mode.Onboarding,
),
)
val route = OnboardingHelper.whereToNavigate(previousChainResult)
ScanChainException.OnboardingNeeded(route).left()
).left()
}
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) {
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()
}
ScanChainException.OnboardingNeeded(
AppRoute.Onboarding(
scanResponse = previousChainResult,
mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin,
),
).left()
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
previousChainResult.right()

View file

@ -7,7 +7,6 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.wallet.R
@ -16,13 +15,8 @@ import org.rekotlin.Action
object DemoHelper {
val config = DemoConfig()
private val demoMiddlewares = listOf(
DemoOnboardingNoteMiddleware(),
)
private val disabledActionFeatures = listOf(
WalletConnectAction.StartWalletConnect::class.java,
BackupAction.StartBackup::class.java,
)
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)
@ -35,10 +29,6 @@ object DemoHelper {
val scanResponse = getScanResponse(appState) ?: return false
if (!scanResponse.isDemoCard()) return false
demoMiddlewares.forEach {
if (it.tryHandle(config, scanResponse, action)) return true
}
disabledActionFeatures.firstOrNull { it == action::class.java }?.let {
val uiMessageSender = store.inject(DaggerGraphState::uiMessageSender)
uiMessageSender.send(

View file

@ -1,65 +0,0 @@
package com.tangem.tap.features.demo
import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
internal class DemoOnboardingNoteMiddleware : DemoMiddleware {
override fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean {
val noteState = store.state.onboardingNoteState
return when (action) {
is OnboardingNoteAction.Balance.Update -> {
val walletManager = if (noteState.walletManager != null) {
noteState.walletManager
} else {
val wmFactory = runBlocking {
store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync()
}
val walletManager = wmFactory?.makePrimaryWalletManager(scanResponse).guard {
return false
}
store.dispatch(OnboardingNoteAction.SetWalletManager(walletManager))
walletManager
}
val balanceAmount = config.getBalance(walletManager.wallet.blockchain)
val loadedBalance = noteState.walletBalance.copy(
value = balanceAmount.value!!,
currency = Currency.Blockchain(walletManager.wallet.blockchain, null),
state = ProgressState.Done,
error = null,
criticalError = null,
)
walletManager.wallet.setAmount(balanceAmount)
scope.launch {
withMainContext {
store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance))
store.dispatch(OnboardingNoteAction.Balance.SetCriticalError(loadedBalance.criticalError))
store.dispatch(OnboardingNoteAction.Balance.SetNonCriticalError(loadedBalance.error))
}
}
return true
}
else -> false
}
}
}

View file

@ -20,7 +20,6 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
@ -33,8 +32,6 @@ import com.tangem.tap.features.details.ui.cardsettings.CardSettingsScreenState
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
import com.tangem.tap.features.details.ui.common.utils.*
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.R
@ -56,7 +53,6 @@ internal class CardSettingsModel @Inject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
) : Model() {
private val params = paramsContainer.require<CardSettingsComponent.Params>()
@ -200,23 +196,13 @@ internal class CardSettingsModel @Inject constructor(
}
if (scanResponse.cardTypesResolver.isTangemTwins()) {
if (onboardingV2FeatureToggles.isTwinRefactoringEnabled) {
store.dispatchNavigationAction {
push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.RecreateWalletTwin,
),
)
}
} else {
// needs to prepare twin state if it's twin cards (depends on onboarding refactoring)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse)))
cardSettingsInteractor.clear()
store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) }
store.dispatchNavigationAction {
push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.RecreateWalletTwin,
),
)
}
} else {
val card = scanResponse.card

View file

@ -21,6 +21,7 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.onramp.FetchHotCryptoUseCase
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.promo.models.StoryContentIds
@ -31,6 +32,9 @@ import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
@ -58,6 +62,7 @@ internal class MainViewModel @Inject constructor(
private val getStoryContentUseCase: GetStoryContentUseCase,
private val imagePreloader: ImagePreloader,
private val fetchHotCryptoUseCase: FetchHotCryptoUseCase,
private val onboardingRepository: OnboardingRepository,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() {
@ -94,6 +99,13 @@ internal class MainViewModel @Inject constructor(
preloadImages()
}
fun checkForUnfinishedBackup() {
viewModelScope.launch(dispatchers.main) {
val onboardingScanResponse = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound(onboardingScanResponse)))
}
}
/** Loading the resources needed to run the application */
private fun loadApplicationResources() {
viewModelScope.launch(dispatchers.main) {

View file

@ -1,13 +1,10 @@
package com.tangem.tap.features.onboarding
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.redux.StateDialog
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingDialog : StateDialog {
object TwinningProcessNotCompleted : OnboardingDialog()
data class InterruptOnboarding(val onOk: VoidCallback) : OnboardingDialog()
data class WalletActivationError(val onConfirm: () -> Unit) : OnboardingDialog()
}

View file

@ -1,46 +1,23 @@
package com.tangem.tap.features.onboarding
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
object OnboardingHelper {
suspend fun isOnboardingCase(response: ScanResponse): Boolean {
val onboardingManager =
store.state.globalState.onboardingState.onboardingManager ?: OnboardingManager(response)
val cardRepository = store.inject(DaggerGraphState::cardRepository)
val cardId = response.card.cardId
return when {
response.cardTypesResolver.isVisaWallet() -> {
if (response.visaCardActivationStatus == null) error("Visa card activation status is null")
@ -52,227 +29,20 @@ object OnboardingHelper {
if (!response.twinsIsTwinned()) {
true
} else {
onboardingManager.isActivationInProgress(cardId)
cardRepository.isActivationInProgress(cardId)
}
}
response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> {
val emptyWallets = response.card.wallets.isEmpty()
val activationInProgress = onboardingManager.isActivationInProgress(cardId)
val activationInProgress = cardRepository.isActivationInProgress(cardId)
val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup &&
!DemoHelper.isDemoCard(response)
emptyWallets || activationInProgress || isNoBackup
}
response.card.wallets.isNotEmpty() -> onboardingManager.isActivationInProgress(cardId)
response.card.wallets.isNotEmpty() -> cardRepository.isActivationInProgress(cardId)
else -> true
}
}
@Suppress("CyclomaticComplexMethod")
fun whereToNavigate(scanResponse: ScanResponse): AppRoute {
val newOnboardingSupportTypes = scanResponse.productType == ProductType.Wallet2 ||
scanResponse.productType == ProductType.Ring ||
scanResponse.productType == ProductType.Wallet ||
scanResponse.productType == ProductType.Visa // AppRoute.OnboardingOther is also supported
val featureToggles = store.inject(DaggerGraphState::onboardingV2FeatureToggles)
if (featureToggles.isOnboardingV2Enabled &&
newOnboardingSupportTypes
) {
val backupState = store.state.onboardingWalletState.backupState
return AppRoute.Onboarding(
scanResponse = scanResponse,
mode = when (backupState.startedSource) {
BackupStartedSource.Onboarding -> AppRoute.Onboarding.Mode.Onboarding
BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackupWallet1
},
)
}
return when (val type = scanResponse.productType) {
ProductType.Note -> if (featureToggles.isNoteRefactoringEnabled) {
AppRoute.Onboarding(scanResponse = scanResponse)
} else {
AppRoute.OnboardingNote
}
ProductType.Wallet,
ProductType.Wallet2,
ProductType.Ring,
-> if (scanResponse.card.settings.isBackupAllowed) {
AppRoute.OnboardingWallet()
} else {
AppRoute.OnboardingOther
}
ProductType.Twins -> if (featureToggles.isTwinRefactoringEnabled) {
AppRoute.Onboarding(scanResponse = scanResponse)
} else {
AppRoute.OnboardingTwins
}
ProductType.Start2Coin,
ProductType.Visa,
-> throw UnsupportedOperationException("Onboarding for ${type.name} cards is not supported")
}
}
suspend fun saveWallet(
alreadyCreatedWallet: UserWallet?,
scanResponse: ScanResponse,
accessCode: String? = null,
backupCardsIds: List<String>? = null,
hasBackupError: Boolean = false,
) {
Analytics.setContext(scanResponse)
val settingsRepository = store.inject(DaggerGraphState::settingsRepository)
when {
// When should save user wallets, then save card without navigate to save wallet screen
store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
store.dispatchWithMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
store.dispatchWithMain(
SaveWalletAction.SaveWalletAfterBackup(
hasBackupError = hasBackupError,
shouldNavigateToWallet = false,
),
)
}
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen
tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet)
delay(timeMillis = 1_200)
store.dispatchWithMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet)
}
}
fun trySaveWalletAndNavigateToWalletScreen(
scanResponse: ScanResponse,
accessCode: String? = null,
backupCardsIds: List<String>? = null,
hasBackupError: Boolean = false,
) {
Analytics.setContext(scanResponse)
scope.launch {
val settingsRepository = store.inject(DaggerGraphState::settingsRepository)
when {
// When should save user wallets, then save card without navigate to save wallet screen
store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
store.dispatchWithMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
store.dispatchWithMain(
SaveWalletAction.SaveWalletAfterBackup(
hasBackupError = hasBackupError,
shouldNavigateToWallet = true,
),
)
}
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen
tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
delay(timeMillis = 1_200)
store.dispatchOnMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
delay(timeMillis = 1_800)
store.dispatchNavigationAction { push(AppRoute.SaveWallet) }
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
}
}
}
}
fun onInterrupted() {
Analytics.removeContext()
}
fun sendToppedUpEvent(scanResponse: ScanResponse) {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
val currency = ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)
if (userWalletId != null && currency != null) {
Analytics.send(Basic.ToppedUp(userWalletId.stringValue, currency))
}
}
fun handleTopUpAction(walletManager: WalletManager, scanResponse: ScanResponse) {
val blockchain = walletManager.wallet.blockchain
val excludedBlockchains = store.inject(DaggerGraphState::excludedBlockchains)
val cryptoCurrency = CryptoCurrencyFactory(excludedBlockchains).createCoin(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
) ?: return
val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
store.dispatchOpenUrl(topUpUrl)
}
private suspend fun proceedWithScanResponse(
scanResponse: ScanResponse,
backupCardsIds: List<String>?,
hasBackupError: Boolean,
alreadyCreatedWallet: UserWallet? = null,
) {
val userWalletBuilderFactory = store.inject(DaggerGraphState::userWalletBuilderFactory)
val userWallet = alreadyCreatedWallet ?: userWalletBuilderFactory.create(scanResponse)
.hasBackupError(hasBackupError)
.backupCardsIds(backupCardsIds?.toSet())
.build()
.guard {
Timber.e("User wallet not created")
return
}
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.save(userWallet, canOverride = true)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess {
mainScope.launch { store.onUserWalletSelected(userWallet) }
store.dispatchWithMain(OnboardingWalletAction.WalletSaved(userWallet.walletId))
}
}
}

View file

@ -1,141 +1,6 @@
package com.tangem.tap.features.onboarding
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.domain.model.hasPendingTransactions
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.extensions.isPositive
import timber.log.Timber
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@Deprecated("Remove when navigation refactoring will be implemented")
class OnboardingManager(var scanResponse: ScanResponse) {
private val cardRepository: CardRepository by lazy { store.inject(DaggerGraphState::cardRepository) }
private val onlineCardVerifier: OnlineCardVerifier by lazy { store.inject(DaggerGraphState::onlineCardVerifier) }
private var cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null
suspend fun loadArtworkUrl(): String {
val cardInfo = cardInfo
?: onlineCardVerifier.getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey)
this.cardInfo = cardInfo
return scanResponse.card.getOrLoadCardArtworkUrl(cardInfo, onlineCardVerifier)
}
suspend fun updateBalance(walletManager: WalletManager): OnboardingWalletBalance {
val isDemoCard = scanResponse.isDemoCard()
val balance = when (val result = walletManager.safeUpdate(isDemoCard)) {
is Result.Success -> {
val wallet = walletManager.wallet
val valueOfAmount = wallet.amounts[AmountType.Coin]?.value
if (valueOfAmount == null) {
val customError = TapError.CustomError("Amount is NULL")
OnboardingWalletBalance.criticalError(customError)
} else {
val balance = if (valueOfAmount.isZero()) {
OnboardingWalletBalance.done(valueOfAmount, wallet.hasPendingTransactions())
} else {
OnboardingWalletBalance.done(valueOfAmount, false)
}
balance
}
}
is Result.Failure -> {
val error = result.error as? TapError ?: TapError.UnknownError
when (error) {
is TapError.WalletManager.NoAccountError -> OnboardingWalletBalance.error(error)
// NoInternetConnection, WalletManager.InternalError
else -> {
Timber.e(error.localizedMessage)
OnboardingWalletBalance.criticalError(TapError.WalletManager.BlockchainIsUnreachableTryLater)
}
}
}
}
return balance.copy(
currency = Currency.Blockchain(
blockchain = walletManager.wallet.blockchain,
derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath,
),
)
}
suspend fun startActivation(cardId: String) {
cardRepository.startCardActivation(cardId)
}
suspend fun finishActivation(cardId: String) {
cardRepository.finishCardActivation(cardId)
}
suspend fun finishActivation(cardIds: List<String>) {
cardRepository.finishCardsActivation(cardIds)
}
suspend fun isActivationStarted(cardId: String): Boolean = cardRepository.isActivationStarted(cardId)
suspend fun isActivationFinished(cardId: String): Boolean = cardRepository.isActivationFinished(cardId)
suspend fun isActivationInProgress(cardId: String): Boolean = cardRepository.isActivationInProgress(cardId)
}
data class OnboardingWalletBalance(
val value: BigDecimal = BigDecimal.ZERO,
val currency: Currency = Currency.Blockchain(Blockchain.Unknown, null),
val hasIncomingTransaction: Boolean = false,
val state: ProgressState,
val error: TapError? = null,
val criticalError: TapError? = null,
) {
val amountToCreateAccount: String?
get() = if (error is TapError.WalletManager.NoAccountError) error.customMessage else null
fun balanceIsToppedUp(): Boolean = value.isPositive() || hasIncomingTransaction
companion object {
fun error(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
error = error,
)
fun criticalError(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
criticalError = error,
)
fun loading(value: BigDecimal = BigDecimal.ZERO): OnboardingWalletBalance = OnboardingWalletBalance(
value,
state = ProgressState.Loading,
)
fun done(value: BigDecimal, hasTransactions: Boolean): OnboardingWalletBalance = OnboardingWalletBalance(
value,
hasIncomingTransaction = hasTransactions,
state = ProgressState.Done,
)
}
}
class OnboardingManager(var scanResponse: ScanResponse)

View file

@ -1,47 +0,0 @@
package com.tangem.tap.features.onboarding
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R
import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
*/
class OnboardingMenuProvider(
private val scanResponseProvider: Provider<ScanResponse>,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.menu_onboarding, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.menu_item_chat_support -> {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
val cardInfo = store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponseProvider()).getOrNull()
?: error("CardInfo must be not null")
scope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.DirectUserRequest(cardInfo))
}
true
}
else -> false
}
}

View file

@ -1,54 +0,0 @@
package com.tangem.tap.features.onboarding.products
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.transitions.HomeToOnboardingTransition
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.onboarding.OnboardingMenuProvider
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentOnboardingMainBinding
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
abstract class BaseOnboardingFragment<T> : BaseStoreFragment(R.layout.fragment_onboarding_main), StoreSubscriber<T> {
protected val binding: FragmentOnboardingMainBinding by viewBinding(FragmentOnboardingMainBinding::bind)
protected val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
override fun configureTransitions() {
enterTransition = HomeToOnboardingTransition()
exitTransition = HomeToOnboardingTransition()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
}
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider(
scanResponseProvider = Provider {
store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: store.state.detailsState.scanResponse
?: error("ScanResponse must be not null")
},
)
protected fun showConfetti(show: Boolean) = with(binding.vConfetti) {
lavConfetti.show(show)
if (show) {
lavConfetti.playAnimation()
} else {
lavConfetti.cancelAnimation()
}
}
}

View file

@ -1,228 +0,0 @@
package com.tangem.tap.features.onboarding.products.note
import android.os.Bundle
import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import coil.load
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.ShareElement
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteStep
import com.tangem.tap.store
import com.tangem.utils.extensions.stripZeroPlainString
import com.tangem.wallet.R
@Deprecated("Use OnboardingNoteComponent")
class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
private var previousStep: OnboardingNoteStep = OnboardingNoteStep.None
private val mainBinding by lazy { binding.vMain }
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
mainBinding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard
binding.toolbar.setTitle(R.string.onboarding_title)
btnRefreshBalanceWidget = RefreshBalanceWidget(mainBinding.onboardingTopContainer.onboardingWalletContainer)
store.dispatch(OnboardingNoteAction.Init)
store.dispatch(OnboardingNoteAction.LoadCardArtwork)
store.dispatch(OnboardingNoteAction.DetermineStepOfScreen)
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.onboardingNoteState == newState.onboardingNoteState
}.select { it.onboardingNoteState }
}
storeSubscribersList.add(this)
}
override fun newState(state: OnboardingNoteState) {
if (activity == null || view == null) return
mainBinding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
pbBinding.pbState.max = state.steps.size - 1
pbBinding.pbState.progress = state.progress
when (state.currentStep) {
OnboardingNoteStep.None -> setupNoneState()
OnboardingNoteStep.CreateWallet -> setupCreateWalletState(state)
OnboardingNoteStep.TopUpWallet -> setupTopUpWalletState(state)
OnboardingNoteStep.Done -> setupDoneState(state)
}
setBalance(state)
showConfetti(state.showConfetti)
previousStep = state.currentStep
}
private fun setBalance(state: OnboardingNoteState) {
if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return
with(mainBinding.onboardingTopContainer.onboardingTvBalance) {
if (state.balanceCriticalError == null) {
val balanceValue = state.walletBalance.value.stripZeroPlainString()
tvBalanceValue.text = balanceValue
tvBalanceCurrency.text = state.walletBalance.currency.currencySymbol
} else {
tvBalanceValue.text = ""
tvBalanceCurrency.text = ""
}
}
}
private fun setupNoneState() = with(mainBinding.onboardingActionContainer) {
btnMainAction.isVisible = false
btnAlternativeAction.isVisible = false
btnRefreshBalanceWidget.show(false)
tvHeader.isVisible = false
tvBody.isVisible = false
btnMainAction.text = ""
btnMainAction.icon = null
btnAlternativeAction.text = ""
btnAlternativeAction.icon = null
tvHeader.text = ""
tvBody.text = ""
}
private fun setupCreateWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(OnboardingNoteAction.CreateWallet)
}
btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean)
btnAlternativeAction.setOnClickListener { }
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
tvHeader.setText(R.string.onboarding_create_wallet_header)
tvBody.setText(R.string.onboarding_create_wallet_body)
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_circle),
)
updateConstraints(state.currentStep, R.layout.lp_onboarding_create_wallet)
btnAlternativeAction.isVisible = false // temporary
}
private fun setupTopUpWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) {
if (availableForBuy(state.scanResponse, state.walletBalance)) {
btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
store.dispatch(OnboardingNoteAction.TopUp)
}
btnAlternativeAction.isVisible = true
btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address)
btnAlternativeAction.setOnClickListener {
store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog)
}
} else {
btnMainAction.setText(R.string.onboarding_button_receive_crypto)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog)
}
btnAlternativeAction.isVisible = false
}
tvHeader.setText(R.string.onboarding_topup_title)
if (state.balanceNonCriticalError == null) {
tvBody.setText(R.string.onboarding_top_up_body)
} else {
state.walletBalance.amountToCreateAccount?.let { amount ->
val tvBodyMessage = getString(
R.string.onboarding_top_up_body_no_account_error,
amount,
state.walletBalance.currency.currencySymbol,
)
tvBody.text = tvBodyMessage
}
}
btnRefreshBalanceWidget.changeState(state.walletBalance.state)
if (btnRefreshBalanceWidget.isShowing != true) {
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(OnboardingNoteAction.Balance.Update)
}
}
val drawable = requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(drawable)
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet)
}
private fun setupDoneState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingNoteAction.Done)
}
btnAlternativeAction.isVisible = false
btnAlternativeAction.text = ""
btnAlternativeAction.setOnClickListener { }
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
tvHeader.setText(R.string.onboarding_done_header)
tvBody.setText(R.string.onboarding_done_body)
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8),
)
updateConstraints(state.currentStep, R.layout.lp_onboarding_done_activation)
}
private fun updateConstraints(currentStep: OnboardingNoteStep, @LayoutRes layoutId: Int) {
if (this.previousStep == currentStep) return
with(mainBinding.onboardingTopContainer) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboardingWalletContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
}
}
private fun availableForBuy(scanResponse: ScanResponse?, walletBalance: OnboardingWalletBalance): Boolean {
scanResponse ?: return false
return store.state.globalState.exchangeManager.availableForBuy(scanResponse, walletBalance.currency)
}
override fun handleOnBackPressed() {
store.dispatch(OnboardingNoteAction.OnBackPressed)
}
}

View file

@ -1,39 +0,0 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingNoteAction : Action {
// from user, ui
object Init : OnboardingNoteAction()
object LoadCardArtwork : OnboardingNoteAction()
object CreateWallet : OnboardingNoteAction()
object TopUp : OnboardingNoteAction()
object ShowAddressInfoDialog : OnboardingNoteAction()
object OnBackPressed : OnboardingNoteAction()
// from redux
class SetArtworkUrl(val artworkUrl: String) : OnboardingNoteAction()
data class SetWalletManager(val walletManager: WalletManager) : OnboardingNoteAction()
object DetermineStepOfScreen : OnboardingNoteAction()
object Done : OnboardingNoteAction()
data class SetStepOfScreen(val step: OnboardingNoteStep) : OnboardingNoteAction()
sealed class Balance {
object Update : OnboardingNoteAction()
data class Set(val balance: OnboardingWalletBalance) : OnboardingNoteAction()
data class SetCriticalError(val error: TapError?) : OnboardingNoteAction()
data class SetNonCriticalError(val error: TapError?) : OnboardingNoteAction()
}
sealed class Confetti {
object Show : OnboardingNoteAction()
object Hide : OnboardingNoteAction()
}
}

View file

@ -1,197 +0,0 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
object OnboardingNoteMiddleware {
val handler = onboardingNoteMiddleware
}
private val onboardingNoteMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleNoteAction(state, action, dispatch)
next(action)
}
}
}
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch: DispatchFunction) {
if (action !is OnboardingNoteAction) return
if (DemoHelper.tryHandle(appState, action)) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager ?: return
val scanResponse = onboardingManager.scanResponse
val card = onboardingManager.scanResponse.card
val noteState = store.state.onboardingNoteState
when (action) {
is OnboardingNoteAction.Init -> {
scope.launch {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
}
is OnboardingNoteAction.LoadCardArtwork -> {
scope.launch {
val artworkUrl = onboardingManager.loadArtworkUrl()
withMainContext { store.dispatch(OnboardingNoteAction.SetArtworkUrl(artworkUrl)) }
}
}
is OnboardingNoteAction.DetermineStepOfScreen -> {
val step = when {
card.wallets.isEmpty() -> OnboardingNoteStep.CreateWallet
noteState.walletBalance.balanceIsToppedUp() -> OnboardingNoteStep.Done
else -> OnboardingNoteStep.TopUpWallet
}
store.dispatch(OnboardingNoteAction.SetStepOfScreen(step))
}
is OnboardingNoteAction.SetStepOfScreen -> {
when (action.step) {
OnboardingNoteStep.CreateWallet -> {
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
}
OnboardingNoteStep.TopUpWallet -> {
Analytics.send(Onboarding.Topup.ScreenOpened())
store.dispatch(OnboardingNoteAction.Balance.Update)
}
OnboardingNoteStep.Done -> {
Analytics.send(Onboarding.Finished())
mainScope.launch {
onboardingManager.finishActivation(card.cardId)
postUi(DELAY_SDK_DIALOG_CLOSE) { store.dispatch(OnboardingNoteAction.Confetti.Show) }
}
}
else -> Unit
}
}
is OnboardingNoteAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createProductWallet(scanResponse)
withMainContext {
when (result) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
val updatedResponse = scanResponse.copy(card = result.data.card)
onboardingManager.scanResponse = updatedResponse
onboardingManager.startActivation(updatedResponse.card.cardId)
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.TopUpWallet))
}
is CompletionResult.Failure -> Unit
}
}
}
}
is OnboardingNoteAction.Balance.Update -> {
val walletManager = if (noteState.walletManager != null) {
noteState.walletManager
} else {
val wmFactory = runBlocking {
store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync()
}
val walletManager = wmFactory?.makePrimaryWalletManager(scanResponse).guard {
val message = "Loading cancelled. Cause: wallet manager didn't created"
val customError = TapError.CustomError(message)
store.dispatchErrorNotification(customError)
return
}
dispatch(OnboardingNoteAction.SetWalletManager(walletManager))
walletManager
}
val isLoadedBefore = noteState.walletBalance.state != ProgressState.Loading
val balanceIsLoading = noteState.walletBalance.copy(
currency = Currency.Blockchain(
walletManager.wallet.blockchain,
walletManager.wallet.publicKey.derivationPath?.rawPath,
),
state = ProgressState.Loading,
error = null,
criticalError = null,
)
store.dispatch(OnboardingNoteAction.Balance.Set(balanceIsLoading))
scope.launch {
val loadedBalance = onboardingManager.updateBalance(walletManager)
delay(if (isLoadedBefore) 0 else 300)
loadedBalance.criticalError?.let { store.dispatchErrorNotification(it) }
withMainContext {
store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance))
store.dispatch(OnboardingNoteAction.Balance.SetCriticalError(loadedBalance.criticalError))
store.dispatch(OnboardingNoteAction.Balance.SetNonCriticalError(loadedBalance.error))
}
}
}
is OnboardingNoteAction.Balance.Set -> {
if (action.balance.balanceIsToppedUp()) {
OnboardingHelper.sendToppedUpEvent(scanResponse)
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.Done))
}
}
is OnboardingNoteAction.ShowAddressInfoDialog -> {
val addressData = noteState.walletManager?.getAddressData() ?: return
Analytics.send(Onboarding.Topup.ButtonShowWalletAddress())
val appDialog = AppDialog.AddressInfoDialog(noteState.walletBalance.currency, addressData)
store.dispatchDialogShow(appDialog)
}
is OnboardingNoteAction.TopUp -> {
val walletManager = noteState.walletManager.guard {
store.dispatchDebugErrorNotification("NPE: WalletManager")
return
}
OnboardingHelper.handleTopUpAction(
walletManager = walletManager,
scanResponse = scanResponse,
)
}
is OnboardingNoteAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)
}
is OnboardingNoteAction.OnBackPressed -> {
store.dispatchDialogShow(
OnboardingDialog.InterruptOnboarding(
onOk = {
OnboardingHelper.onInterrupted()
store.dispatchNavigationAction(AppRouter::pop)
},
),
)
}
else -> Unit
}
}

View file

@ -1,47 +0,0 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import org.rekotlin.Action
object OnboardingNoteReducer {
fun reduce(action: Action, state: AppState): OnboardingNoteState = internalReduce(action, state)
}
private fun internalReduce(action: Action, appState: AppState): OnboardingNoteState {
var state = appState.onboardingNoteState
when (action) {
is GlobalAction.Onboarding.Start -> {
state = OnboardingNoteState(scanResponse = action.scanResponse)
}
is OnboardingNoteAction.SetArtworkUrl -> {
state = state.copy(cardArtworkUrl = action.artworkUrl)
}
is OnboardingNoteAction.SetWalletManager -> {
state = state.copy(walletManager = action.walletManager)
}
is OnboardingNoteAction.Balance.Set -> {
state = state.copy(walletBalance = action.balance)
}
is OnboardingNoteAction.Balance.SetCriticalError -> {
state = state.copy(balanceCriticalError = action.error)
}
is OnboardingNoteAction.Balance.SetNonCriticalError -> {
state = state.copy(balanceNonCriticalError = action.error)
}
is OnboardingNoteAction.SetStepOfScreen -> {
if (action.step != state.currentStep && state.steps.contains(action.step)) {
state = state.copy(currentStep = action.step)
}
}
is OnboardingNoteAction.Confetti.Show -> {
state = state.copy(showConfetti = true)
}
is OnboardingNoteAction.Confetti.Hide -> {
state = state.copy(showConfetti = false)
}
}
return state
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class OnboardingNoteState(
val walletManager: WalletManager? = null,
// UI
val cardArtworkUrl: String? = null,
val walletBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading(),
val balanceNonCriticalError: TapError? = null,
val balanceCriticalError: TapError? = null,
val currentStep: OnboardingNoteStep = OnboardingNoteStep.None,
val steps: List<OnboardingNoteStep> = OnboardingNoteStep.values().toList(),
val showConfetti: Boolean = false,
val scanResponse: ScanResponse? = null,
) : StateType {
val progress: Int
get() = steps.indexOf(currentStep)
}
enum class OnboardingNoteStep {
None, CreateWallet, TopUpWallet, Done
}

View file

@ -1,128 +0,0 @@
package com.tangem.tap.features.onboarding.products.otherCards
import android.os.Bundle
import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import coil.load
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.ShareElement
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsAction
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsState
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsStep
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCardsState>() {
private val mainBinding by lazy { binding.vMain }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
mainBinding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard
binding.toolbar.setTitle(R.string.onboarding_title)
store.dispatch(OnboardingOtherCardsAction.Init)
store.dispatch(OnboardingOtherCardsAction.LoadCardArtwork)
store.dispatch(OnboardingOtherCardsAction.DetermineStepOfScreen)
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.onboardingOtherCardsState == newState.onboardingOtherCardsState
}.select { it.onboardingOtherCardsState }
}
storeSubscribersList.add(this)
}
override fun newState(state: OnboardingOtherCardsState) {
if (activity == null || view == null) return
if (state.currentStep == OnboardingOtherCardsStep.None) return
mainBinding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
pbBinding.pbState.max = state.steps.size - 1
pbBinding.pbState.progress = state.progress
when (state.currentStep) {
OnboardingOtherCardsStep.CreateWallet -> setupCreateWalletState()
OnboardingOtherCardsStep.Done -> setupDoneState()
else -> {}
}
showConfetti(state.showConfetti)
}
private fun setupCreateWalletState() = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(OnboardingOtherCardsAction.CreateWallet)
}
btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean)
btnAlternativeAction.setOnClickListener { }
tvHeader.setText(R.string.onboarding_create_wallet_header)
tvBody.setText(R.string.onboarding_create_wallet_body)
btnAlternativeAction.isVisible = false // temporary
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_circle),
)
updateConstraints(R.layout.lp_onboarding_create_wallet)
}
private fun setupDoneState() = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingOtherCardsAction.Done)
}
btnAlternativeAction.isVisible = false
btnAlternativeAction.text = ""
btnAlternativeAction.setOnClickListener { }
tvHeader.setText(R.string.onboarding_done_header)
tvBody.setText(R.string.onboarding_done_body)
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8),
)
updateConstraints(R.layout.lp_onboarding_done)
}
private fun updateConstraints(@LayoutRes layoutId: Int) = with(mainBinding.onboardingTopContainer) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboardingWalletContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
}
override fun handleOnBackPressed() {
store.dispatch(OnboardingOtherCardsAction.OnBackPressed)
super.handleOnBackPressed()
}
}

View file

@ -1,26 +0,0 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingOtherCardsAction : Action {
// from user, ui
object Init : OnboardingOtherCardsAction()
object LoadCardArtwork : OnboardingOtherCardsAction()
object CreateWallet : OnboardingOtherCardsAction()
// from redux
class SetArtworkUrl(val artworkUrl: String) : OnboardingOtherCardsAction()
object DetermineStepOfScreen : OnboardingOtherCardsAction()
object Done : OnboardingOtherCardsAction()
object OnBackPressed : OnboardingOtherCardsAction()
data class SetStepOfScreen(val step: OnboardingOtherCardsStep) : OnboardingOtherCardsAction()
sealed class Confetti {
object Show : OnboardingOtherCardsAction()
object Hide : OnboardingOtherCardsAction()
}
}

View file

@ -1,129 +0,0 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.mainScope
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
object OnboardingOtherCardsMiddleware {
val handler = onboardingOtherCardsMiddleware
}
private val onboardingOtherCardsMiddleware: Middleware<AppState> = { dispatch, _ ->
{ next ->
{ action ->
handleOtherCardsAction(action)
next(action)
}
}
}
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handleOtherCardsAction(action: Action) {
if (action !is OnboardingOtherCardsAction) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager ?: return
val card = onboardingManager.scanResponse.card
when (action) {
is OnboardingOtherCardsAction.Init -> {
scope.launch {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
}
is OnboardingOtherCardsAction.LoadCardArtwork -> {
scope.launch {
val artworkUrl = onboardingManager.loadArtworkUrl()
withMainContext { store.dispatch(OnboardingOtherCardsAction.SetArtworkUrl(artworkUrl)) }
}
}
is OnboardingOtherCardsAction.DetermineStepOfScreen -> {
val step = when {
card.wallets.isEmpty() -> OnboardingOtherCardsStep.CreateWallet
else -> OnboardingOtherCardsStep.Done
}
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(step))
}
is OnboardingOtherCardsAction.SetStepOfScreen -> {
when (action.step) {
OnboardingOtherCardsStep.CreateWallet -> {
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
}
OnboardingOtherCardsStep.Done -> {
Analytics.send(Onboarding.Finished())
mainScope.launch {
onboardingManager.finishActivation(card.cardId)
postUi(200) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) }
}
}
else -> Unit
}
}
is OnboardingOtherCardsAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createProductWallet(
scanResponse = onboardingManager.scanResponse,
shouldReset = globalState.onboardingState.shouldResetOnCreate,
)
withMainContext {
when (result) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
val updatedResponse = onboardingManager.scanResponse.copy(
card = result.data.card,
)
val updatedCard = updatedResponse.card
onboardingManager.scanResponse = updatedResponse
onboardingManager.startActivation(updatedCard.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done))
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
handleActivationError()
}
}
}
}
}
}
OnboardingOtherCardsAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(onboardingManager.scanResponse)
}
OnboardingOtherCardsAction.OnBackPressed -> {
OnboardingHelper.onInterrupted()
}
else -> Unit
}
}
private fun handleActivationError() {
store.dispatchDialogShow(
OnboardingDialog.WalletActivationError(
onConfirm = {
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(true))
},
),
)
}

View file

@ -1,35 +0,0 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import org.rekotlin.Action
object OnboardingOtherCardsReducer {
fun reduce(action: Action, state: AppState): OnboardingOtherCardsState = internalReduce(action, state)
}
private fun internalReduce(action: Action, appState: AppState): OnboardingOtherCardsState {
var state = appState.onboardingOtherCardsState
when (action) {
is GlobalAction.Onboarding.Start -> {
state = OnboardingOtherCardsState()
}
is OnboardingOtherCardsAction.SetArtworkUrl -> {
state = state.copy(cardArtworkUrl = action.artworkUrl)
}
is OnboardingOtherCardsAction.SetStepOfScreen -> {
if (action.step != state.currentStep && state.steps.contains(action.step)) {
state = state.copy(currentStep = action.step)
}
}
is OnboardingOtherCardsAction.Confetti.Show -> {
state = state.copy(showConfetti = true)
}
is OnboardingOtherCardsAction.Confetti.Hide -> {
state = state.copy(showConfetti = false)
}
}
return state
}

View file

@ -1,22 +0,0 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class OnboardingOtherCardsState(
// UI
val cardArtworkUrl: String? = null,
val showConfetti: Boolean = false,
val currentStep: OnboardingOtherCardsStep = OnboardingOtherCardsStep.None,
val steps: List<OnboardingOtherCardsStep> = OnboardingOtherCardsStep.values().toList(),
) : StateType {
val progress: Int
get() = steps.indexOf(currentStep)
}
enum class OnboardingOtherCardsStep {
None, CreateWallet, Done
}

View file

@ -1,66 +0,0 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.Message
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class TwinCardsAction : Action {
data class IfTwinsPrepareState(val scanResponse: ScanResponse) : TwinCardsAction()
object Init : TwinCardsAction()
data class SetMode(val mode: CreateTwinWalletMode) : TwinCardsAction()
data class SetStepOfScreen(val step: TwinCardsStep) : TwinCardsAction()
data class SetUserUnderstand(val isUnderstand: Boolean) : TwinCardsAction()
sealed class CardsManager {
data class Set(val manager: TwinCardsManager) : TwinCardsAction()
object Release : TwinCardsAction()
}
sealed class Wallet : TwinCardsAction() {
data class LaunchFirstStep(val initialMessage: Message) : TwinCardsAction()
data class LaunchSecondStep(
val initialMessage: Message,
val preparingMessage: Message,
val creatingWalletMessage: Message,
) : TwinCardsAction()
data class LaunchThirdStep(val message: Message) : TwinCardsAction()
}
// for the onboarding
data class SetPairCardId(val cardId: String) : TwinCardsAction()
object TopUp : TwinCardsAction()
object ShowAddressInfoDialog : TwinCardsAction()
data class SetWalletManager(val walletManager: WalletManager) : TwinCardsAction()
object Done : TwinCardsAction()
data class OnBackPressed(
// this is necessary for the correct animation of the return of cards to the Home screen
val shouldResetTwinCardsWidget: (should: Boolean, popAction: VoidCallback) -> Unit,
) : TwinCardsAction()
data class SaveScannedTwinCardAndNavigateToWallet(
val scanResponse: ScanResponse,
) : TwinCardsAction()
sealed class Balance {
object Update : TwinCardsAction()
data class Set(val balance: OnboardingWalletBalance) : TwinCardsAction()
data class SetCriticalError(val error: TapError?) : TwinCardsAction()
data class SetNonCriticalError(val error: TapError?) : TwinCardsAction()
}
sealed class Confetti {
object Show : TwinCardsAction()
object Hide : TwinCardsAction()
}
}

View file

@ -1,399 +0,0 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
import kotlin.reflect.KClass
object TwinCardsMiddleware {
val handler = twinsWalletMiddleware
}
private val twinsWalletMiddleware: Middleware<AppState> = { dispatch, _ ->
{ next ->
{ action ->
handle(action, dispatch)
next(action)
}
}
}
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handle(action: Action, dispatch: DispatchFunction) {
if (action !is TwinCardsAction) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager
val twinCardsState = store.state.twinCardsState
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
fun getScanResponse(): ScanResponse {
return when (twinCardsState.mode) {
is CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse
is CreateTwinWalletMode.RecreateWallet -> globalState.scanResponse
} ?: throw NullPointerException("ScanResponse can't be NULL")
}
fun updateScanResponse(response: ScanResponse) {
when (twinCardsState.mode) {
is CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response
is CreateTwinWalletMode.RecreateWallet -> store.dispatchOnMain(GlobalAction.SaveScanResponse(response))
}
}
fun startCardActivation(cardId: String) {
if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) {
scope.launch {
onboardingManager?.startActivation(cardId)
}
}
}
fun finishCardActivation() {
if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) {
Analytics.send(Onboarding.Finished())
val cardIds = listOfNotNull(getScanResponse().card.cardId, twinCardsState.pairCardId)
if (cardIds.isNotEmpty()) {
mainScope.launch {
onboardingManager?.finishActivation(cardIds)
}
}
}
}
when (action) {
is TwinCardsAction.Init -> {
mainScope.launch {
if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return@launch
if (twinCardsState.mode is CreateTwinWalletMode.RecreateWallet) {
store.dispatch(GlobalAction.SaveScanResponse(twinCardsState.mode.scanResponse))
}
val scanResponse = getScanResponse()
onboardingManager?.apply {
if (!isActivationStarted(scanResponse.card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
when (twinCardsState.mode) {
is CreateTwinWalletMode.CreateWallet -> {
mainScope.launch {
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
.invokeSync()
val dispatchAction = if (wasTwinsOnboardingShown) {
val step = when {
!scanResponse.twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet
twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done
else -> TwinCardsStep.TopUpWallet
}
TwinCardsAction.SetStepOfScreen(step)
} else {
TwinCardsAction.SetStepOfScreen(TwinCardsStep.Welcome)
}
store.dispatch(dispatchAction)
}
}
is CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning))
}
}
}
}
is TwinCardsAction.SetStepOfScreen -> {
when (action.step) {
is TwinCardsStep.WelcomeOnly, TwinCardsStep.Welcome -> {
Analytics.send(Onboarding.Twins.ScreenOpened())
scope.launch {
store.inject(DaggerGraphState::saveTwinsOnboardingShownUseCase).invoke()
}
}
is TwinCardsStep.CreateFirstWallet -> {
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
Analytics.send(Onboarding.Twins.SetupStarted())
}
TwinCardsStep.TopUpWallet -> {
Analytics.send(Onboarding.Topup.ScreenOpened())
store.dispatch(TwinCardsAction.Balance.Update)
}
TwinCardsStep.Done -> {
finishCardActivation()
postUi(500) { store.dispatch(TwinCardsAction.Confetti.Show) }
}
TwinCardsStep.None,
TwinCardsStep.Warning,
TwinCardsStep.CreateSecondWallet,
TwinCardsStep.CreateThirdWallet,
-> Unit
}
}
is TwinCardsAction.Wallet.LaunchFirstStep -> {
val manager = TwinCardsManager(card = getScanResponse().card)
store.dispatch(TwinCardsAction.CardsManager.Set(manager))
scope.launch {
when (val result = manager.createFirstWallet(action.initialMessage)) {
is CompletionResult.Success -> {
// remove wallet only after first step of retwin
scope.launch {
userWalletsListManager.delete(
listOfNotNull(UserWalletIdBuilder.scanResponse(getScanResponse()).build()),
)
}
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
startCardActivation(result.data.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateSecondWallet))
}
}
is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error)
}
}
}
is TwinCardsAction.Wallet.LaunchSecondStep -> {
val manager = twinCardsState.twinCardsManager ?: return
scope.launch {
val result = manager.createSecondWallet(
action.initialMessage,
action.preparingMessage,
action.creatingWalletMessage,
)
when (result) {
is CompletionResult.Success -> {
startCardActivation(result.data.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
store.dispatch(TwinCardsAction.SetPairCardId(result.data.cardId))
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateThirdWallet))
}
}
is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error)
}
}
}
is TwinCardsAction.Wallet.LaunchThirdStep -> {
val manager = twinCardsState.twinCardsManager ?: return
scope.launch {
when (val result = manager.complete(action.message)) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.Twins.SetupFinished())
updateScanResponse(result.data)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
when (twinCardsState.mode) {
is CreateTwinWalletMode.CreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.TopUpWallet))
}
is CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done))
}
}
}
}
is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error)
}
}
}
is TwinCardsAction.Balance.Update -> {
val walletManager = if (twinCardsState.walletManager != null) {
twinCardsState.walletManager
} else {
val wmFactory = runBlocking {
store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync()
}
val walletManager = wmFactory?.makePrimaryWalletManager(getScanResponse()).guard {
val message = "Loading cancelled. Cause: wallet manager didn't created"
val customError = TapError.CustomError(message)
store.dispatchErrorNotification(customError)
return
}
dispatch(TwinCardsAction.SetWalletManager(walletManager))
walletManager
}
val isLoadedBefore = twinCardsState.walletBalance.state != ProgressState.Loading
val balanceIsLoading = twinCardsState.walletBalance.copy(
currency = Currency.Blockchain(
walletManager.wallet.blockchain,
walletManager.wallet.publicKey.derivationPath?.rawPath,
),
state = ProgressState.Loading,
error = null,
criticalError = null,
)
store.dispatch(TwinCardsAction.Balance.Set(balanceIsLoading))
scope.launch {
val loadedBalance = onboardingManager?.updateBalance(walletManager) ?: return@launch
loadedBalance.criticalError?.let(store::dispatchErrorNotification)
delay(if (isLoadedBefore) 0 else 300)
withMainContext {
store.dispatch(TwinCardsAction.Balance.Set(loadedBalance))
store.dispatch(TwinCardsAction.Balance.SetCriticalError(loadedBalance.criticalError))
store.dispatch(TwinCardsAction.Balance.SetNonCriticalError(loadedBalance.error))
}
}
}
is TwinCardsAction.Balance.Set -> {
if (action.balance.balanceIsToppedUp()) {
OnboardingHelper.sendToppedUpEvent(getScanResponse())
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done))
}
}
is TwinCardsAction.ShowAddressInfoDialog -> {
val addressData = twinCardsState.walletManager?.getAddressData() ?: return
val appDialog = AppDialog.AddressInfoDialog(twinCardsState.walletBalance.currency, addressData)
store.dispatchDialogShow(appDialog)
}
is TwinCardsAction.TopUp -> {
val walletManager = twinCardsState.walletManager.guard {
store.dispatchDebugErrorNotification("NPE: WalletManager")
return
}
OnboardingHelper.handleTopUpAction(
walletManager = walletManager,
scanResponse = onboardingManager?.scanResponse ?: return,
)
}
TwinCardsAction.Done -> {
val scanResponse = getScanResponse()
when (twinCardsState.mode) {
is CreateTwinWalletMode.CreateWallet -> {
store.dispatchOnMain(GlobalAction.Onboarding.Stop)
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)
}
is CreateTwinWalletMode.RecreateWallet -> {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
if (walletsRepository.shouldSaveUserWalletsSync()) {
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)
} else {
store.dispatchNavigationAction { popTo<AppRoute.Home>() }
}
}
}
}
}
is TwinCardsAction.SaveScannedTwinCardAndNavigateToWallet -> {
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(
scanResponse = action.scanResponse,
)
}
is TwinCardsAction.OnBackPressed -> {
if (twinCardsState.twinningInProgress) {
store.dispatchDialogShow(OnboardingDialog.TwinningProcessNotCompleted)
} else {
val onOkCallback = {
val shouldReturnCardBack = twinCardsState.mode == CreateTwinWalletMode.CreateWallet &&
twinCardsState.currentStep != TwinCardsStep.TopUpWallet &&
twinCardsState.currentStep != TwinCardsStep.Done
OnboardingHelper.onInterrupted()
store.dispatch(TwinCardsAction.CardsManager.Release)
action.shouldResetTwinCardsWidget(shouldReturnCardBack) {
store.dispatchNavigationAction { popTo(routeClass = getPopBackScreen()) }
}
}
store.dispatchDialogShow(OnboardingDialog.InterruptOnboarding(onOkCallback))
}
}
else -> Unit
}
}
private fun showCardVerificationFailedDialog(error: TangemError) {
if (error is TangemSdkError.CardVerificationFailed) {
Analytics.send(
event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed(
AnalyticsParam.ScreensSources.Onboarding,
),
)
val resource = error.localizedDescriptionRes()
val resId = resource.resId ?: R.string.common_unknown_error
val resArgs = resource.args.map { it.value }
store.inject(DaggerGraphState::uiMessageSender).send(
message = Dialogs.cardVerificationFailed(
errorDescription = resourceReference(id = resId, resArgs.toWrappedList()),
onRequestSupport = {
mainScope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase).invoke(
type = FeedbackEmailType.CardAttestationFailed,
)
}
},
),
)
}
}
private fun getPopBackScreen(): KClass<out AppRoute> {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
return if (userWalletsListManager.hasUserWallets) {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false }
if (isLocked) {
AppRoute.Welcome::class
} else {
AppRoute.Wallet::class
}
} else {
AppRoute.Home::class
}
}

View file

@ -1,77 +0,0 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
object TwinCardsReducer {
fun reduce(action: Action, state: AppState): TwinCardsState = internalReduce(action, state)
}
@Suppress("ComplexMethod")
private fun internalReduce(action: Action, state: AppState): TwinCardsState {
if (action !is TwinCardsAction) return state.twinCardsState
var state = state.twinCardsState
when (action) {
is TwinCardsAction.IfTwinsPrepareState -> {
state = if (action.scanResponse.cardTypesResolver.isTangemTwins()) {
val cardNumber = action.scanResponse.card.getTwinCardNumber() ?: return state
TwinCardsState(
cardNumber = cardNumber,
mode = CreateTwinWalletMode.CreateWallet,
)
} else {
TwinCardsState()
}
}
is TwinCardsAction.SetMode -> {
state = state.copy(
mode = action.mode,
userWasUnderstandIfWalletRecreate = false,
)
}
is TwinCardsAction.SetStepOfScreen -> {
state = state.copy(
currentStep = action.step,
welcomeOnlyScanResponse = (action.step as? TwinCardsStep.WelcomeOnly)?.scanResponse,
)
}
is TwinCardsAction.SetUserUnderstand -> {
state = state.copy(userWasUnderstandIfWalletRecreate = action.isUnderstand)
}
is TwinCardsAction.CardsManager.Set -> {
state = state.copy(twinCardsManager = action.manager)
}
TwinCardsAction.CardsManager.Release -> {
state = state.copy(twinCardsManager = null)
}
is TwinCardsAction.SetPairCardId -> {
state = state.copy(pairCardId = action.cardId)
}
is TwinCardsAction.SetWalletManager -> {
state = state.copy(walletManager = action.walletManager)
}
is TwinCardsAction.Balance.Set -> {
state = state.copy(walletBalance = action.balance)
}
is TwinCardsAction.Balance.SetCriticalError -> {
state = state.copy(balanceCriticalError = action.error)
}
is TwinCardsAction.Balance.SetNonCriticalError -> {
state = state.copy(balanceNonCriticalError = action.error)
}
is TwinCardsAction.Confetti.Show -> {
state = state.copy(showConfetti = true)
}
is TwinCardsAction.Confetti.Hide -> {
state = state.copy(showConfetti = false)
}
else -> {}
}
return state
}

View file

@ -1,82 +0,0 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class TwinCardsState(
// if cardNumber = null -> the scanned card is not Twin
val cardNumber: TwinCardNumber? = null,
val mode: CreateTwinWalletMode = CreateTwinWalletMode.CreateWallet,
val currentStep: TwinCardsStep = TwinCardsStep.Welcome,
val userWasUnderstandIfWalletRecreate: Boolean = false,
// available only when creating or recreating wallets
val twinCardsManager: TwinCardsManager? = null,
// for the onboarding
val pairCardId: String? = null, // available after create\recreate wallets
val walletManager: WalletManager? = null,
val walletBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading(),
val balanceNonCriticalError: TapError? = null,
val balanceCriticalError: TapError? = null,
val showConfetti: Boolean = false,
val welcomeOnlyScanResponse: ScanResponse? = null,
) : StateType {
val steps: List<TwinCardsStep>
get() = when (mode) {
is CreateTwinWalletMode.CreateWallet -> listOf(
TwinCardsStep.None,
TwinCardsStep.CreateFirstWallet,
TwinCardsStep.CreateSecondWallet,
TwinCardsStep.CreateThirdWallet,
TwinCardsStep.TopUpWallet,
TwinCardsStep.Done,
)
is CreateTwinWalletMode.RecreateWallet -> listOf(
TwinCardsStep.None,
TwinCardsStep.CreateFirstWallet,
TwinCardsStep.CreateSecondWallet,
TwinCardsStep.CreateThirdWallet,
TwinCardsStep.Done,
)
}
val progress: Int
get() = steps.indexOf(currentStep)
val twinningInProgress: Boolean
get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet
}
sealed class CreateTwinWalletMode {
data object CreateWallet : CreateTwinWalletMode()
data class RecreateWallet(
val scanResponse: ScanResponse,
) : CreateTwinWalletMode()
}
sealed class TwinCardsStep {
object None : TwinCardsStep()
data class WelcomeOnly(
val scanResponse: ScanResponse,
) : TwinCardsStep()
object Welcome : TwinCardsStep()
object Warning : TwinCardsStep()
object CreateFirstWallet : TwinCardsStep()
object CreateSecondWallet : TwinCardsStep()
object CreateThirdWallet : TwinCardsStep()
// for the onboarding
object TopUpWallet : TwinCardsStep()
object Done : TwinCardsStep()
}

View file

@ -1,460 +0,0 @@
package com.tangem.tap.features.onboarding.products.twins.ui
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import coil.load
import com.tangem.Message
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.ShareElement
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.Artwork
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.OnboardingMenuProvider
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.utils.extensions.stripZeroPlainString
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
import dagger.hilt.android.AndroidEntryPoint
@Suppress("LargeClass")
@AndroidEntryPoint
internal class OnboardingTwinsFragment : BaseOnboardingFragment<TwinCardsState>() {
private val mainBinding by lazy { binding.vMain }
private var previousStep: TwinCardsStep = TwinCardsStep.None
private lateinit var twinsWidget: TwinsCardWidget
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
store.dispatch(TwinCardsAction.Init)
}
override fun configureTransitions() {
when (store.state.twinCardsState.mode) {
is CreateTwinWalletMode.CreateWallet -> {
super.configureTransitions()
}
is CreateTwinWalletMode.RecreateWallet -> {
configureDefaultTransactions()
}
}
}
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider(
scanResponseProvider = Provider { getActualScanResponse() },
)
@Suppress("MagicNumber")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
reconfigureLayoutForTwins(mainBinding.onboardingTopContainer)
val typedValue = TypedValue()
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
val deviceScaleFactorForWelcomeState = typedValue.float
twinsWidget = TwinsCardWidget(
LeapfrogWidget(mainBinding.onboardingTopContainer.cardsContainer),
deviceScaleFactorForWelcomeState,
) {
285f * deviceScaleFactorForWelcomeState
}
btnRefreshBalanceWidget =
RefreshBalanceWidget(mainBinding.onboardingTopContainer.onboardingWalletContainer)
binding.toolbar.title = getText(R.string.twins_recreate_toolbar)
mainBinding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1_URL) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
mainBinding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2_URL) {
placeholder(R.drawable.card_placeholder_white)
error(R.drawable.card_placeholder_white)
fallback(R.drawable.card_placeholder_white)
}
}
override fun onStart() {
super.onStart()
setStatusBarColor(R.color.background_primary)
}
private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) =
with(containerBinding) {
imvFrontCard.hide()
imvCardBackground.hide()
cardsContainer.show()
imvTwinFrontCard.transitionName = ShareElement.imvFrontCard
imvTwinBackCard.transitionName = ShareElement.imvBackCard
// if don't this, the bg_circle_... is overflow the app_bar
binding.appBar.bringToFront()
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.twinCardsState == newState.twinCardsState
}.select { it.twinCardsState }
}
storeSubscribersList.add(this)
}
override fun newState(state: TwinCardsState) {
if (activity == null || view == null) return
pbBinding.pbState.max = state.steps.size - 1
pbBinding.pbState.progress = state.progress
when (state.currentStep) {
is TwinCardsStep.WelcomeOnly -> setupWelcomeOnlyState(state, state.currentStep.scanResponse)
is TwinCardsStep.Welcome -> setupWelcomeState(state)
is TwinCardsStep.Warning -> setupWarningState(state)
is TwinCardsStep.CreateFirstWallet -> setupCreateFirstWalletState(state)
is TwinCardsStep.CreateSecondWallet -> setupCreateSecondWalletState(state)
is TwinCardsStep.CreateThirdWallet -> setupCreateThirdWalletState(state)
is TwinCardsStep.TopUpWallet -> setupTopUpWalletState(state)
is TwinCardsStep.Done -> setupDoneState(state)
else -> {}
}
setBalance(state)
showConfetti(state.showConfetti)
previousStep = state.currentStep
}
private fun setBalance(state: TwinCardsState) {
if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return
with(mainBinding.onboardingTopContainer.onboardingTvBalance) {
if (state.balanceCriticalError == null) {
val balanceValue = state.walletBalance.value.stripZeroPlainString()
tvBalanceValue.text = balanceValue
tvBalanceCurrency.text = state.walletBalance.currency.currencySymbol
} else {
tvBalanceValue.text = ""
tvBalanceCurrency.text = ""
}
}
}
private fun setupWelcomeOnlyState(state: TwinCardsState, scanResponse: ScanResponse) {
setupWelcomeState(state) {
store.dispatch(TwinCardsAction.SaveScannedTwinCardAndNavigateToWallet(scanResponse))
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.None))
}
}
private fun setupWelcomeState(state: TwinCardsState) {
setupWelcomeState(state) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet))
}
}
private fun setupWelcomeState(state: TwinCardsState, onMainButtonClick: VoidCallback) =
with(mainBinding.onboardingActionContainer) {
twinsWidget.toWelcome(false)
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.show()
pbBinding.pbState.hide()
tvHeader.setText(R.string.twins_onboarding_subtitle)
tvBody.text = getString(
R.string.twins_onboarding_description_format,
state.cardNumber?.pairIndexNumber(),
)
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener { onMainButtonClick() }
}
private fun setupWarningState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
twinsWidget.toWelcome(false)
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide()
pbBinding.pbState.hide()
chbUnderstand.show()
tvHeader.setText(R.string.common_warning)
tvBody.setText(R.string.twins_recreate_warning)
chbUnderstand.setOnCheckedChangeListener { _, isChecked ->
store.dispatch(TwinCardsAction.SetUserUnderstand(isChecked))
}
btnMainAction.isEnabled = state.userWasUnderstandIfWalletRecreate
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet))
}
}
private fun setupCreateFirstWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide()
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleLarge.hide()
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMedium.hide()
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMin.hide()
chbUnderstand.hide()
pbBinding.pbState.show()
mainBinding.onboardingTopContainer.onboardingWalletContainer.beginDelayedTransition()
when (previousStep) {
TwinCardsStep.None -> {
twinsWidget.toLeapfrog(false) {
when (state.cardNumber) {
TwinCardNumber.First -> {}
TwinCardNumber.Second -> {
switchToCard(
state.cardNumber,
false,
)
}
else -> {}
}
}
}
TwinCardsStep.Welcome, TwinCardsStep.Warning -> {
twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) })
}
else -> {}
}
val twinIndexNumber = state.cardNumber?.indexNumber()
tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(
TwinCardsAction.Wallet.LaunchFirstStep(
initialMessage = Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
),
)
}
btnAlternativeAction.isVisible = false
btnAlternativeAction.isClickable = false
}
private fun setupCreateSecondWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
switchToCard(state.cardNumber?.pairNumber())
val twinPairIndexNumber = state.cardNumber?.pairIndexNumber()
tvHeader.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber)
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
btnMainAction.text =
getString(R.string.twins_recreate_button_format, twinPairIndexNumber)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
store.dispatch(
TwinCardsAction.Wallet.LaunchSecondStep(
Message(
getString(
R.string.twins_recreate_title_format,
twinPairIndexNumber,
),
),
Message(getString(R.string.twins_recreate_title_preparing)),
Message(getString(R.string.twins_recreate_title_creating_wallet)),
),
)
}
}
private fun setupCreateThirdWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
switchToCard(state.cardNumber)
val twinIndexNumber = state.cardNumber?.indexNumber()
tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btnMainAction.setIconResource(R.drawable.ic_tangem_24)
btnMainAction.setOnClickListener {
store.dispatch(
TwinCardsAction.Wallet.LaunchThirdStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
),
)
}
}
private fun setupTopUpWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
when (previousStep) {
TwinCardsStep.None, TwinCardsStep.Welcome -> {
when (state.cardNumber) {
TwinCardNumber.First -> {
twinsWidget.leapfrogWidget.unfold(true) {
twinsWidget.toActivate(true)
}
}
TwinCardNumber.Second -> {
switchToCard(state.cardNumber, true) {
twinsWidget.toActivate(true)
}
}
else -> {}
}
}
TwinCardsStep.CreateThirdWallet -> {
twinsWidget.toActivate()
}
else -> {}
}
if (availableForBuy(getActualScanResponse(), state.walletBalance)) {
btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto)
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.TopUp)
}
btnAlternativeAction.isVisible = true
btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address)
btnAlternativeAction.setOnClickListener {
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
}
} else {
btnMainAction.setText(R.string.onboarding_button_receive_crypto)
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
}
btnAlternativeAction.isVisible = false
}
tvHeader.setText(R.string.onboarding_topup_title)
tvBody.setText(R.string.onboarding_top_up_body)
btnMainAction.icon = null
btnRefreshBalanceWidget.changeState(state.walletBalance.state)
if (btnRefreshBalanceWidget.isShowing != true) {
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(TwinCardsAction.Balance.Update)
}
}
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8),
)
mainBinding.onboardingTopContainer.imvCardBackground.backgroundTintList =
requireContext().resources.getColorStateList(R.color.onboarding_card_background, null)
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins)
}
private fun setupDoneState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.common_continue)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.Confetti.Hide)
store.dispatch(TwinCardsAction.Done)
}
btnAlternativeAction.isVisible = false
btnAlternativeAction.isClickable = false
tvHeader.setText(R.string.onboarding_done_header)
tvBody.setText(R.string.onboarding_done_body)
val layout = when (state.mode) {
is CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins
is CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done
}
updateConstraints(state.currentStep, layout)
}
private fun updateConstraints(currentStep: TwinCardsStep, @LayoutRes layoutId: Int) {
if (this.previousStep == currentStep) return
with(mainBinding.onboardingTopContainer) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboardingWalletContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
}
}
private fun switchToCard(cardNumber: TwinCardNumber?, animate: Boolean = true, onEnd: VoidCallback = {}) {
val position = cardNumber?.number ?: return
if (twinsWidget.leapfrogWidget.getViewPositionByIndex(1) != position - 1) {
twinsWidget.leapfrogWidget.leap(animate, onEnd)
} else {
onEnd()
}
}
private fun availableForBuy(scanResponse: ScanResponse?, walletBalance: OnboardingWalletBalance): Boolean {
scanResponse ?: return false
return store.state.globalState.exchangeManager.availableForBuy(scanResponse, walletBalance.currency)
}
private fun getActualScanResponse(): ScanResponse {
return store.state.twinCardsState.welcomeOnlyScanResponse
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: store.state.detailsState.scanResponse
?: error("ScanResponse must be not null")
}
override fun handleOnBackPressed() {
store.dispatch(
TwinCardsAction.OnBackPressed { should, popAction ->
store.dispatch(TwinCardsAction.Confetti.Hide)
showConfetti(false)
if (should) {
switchToCard(TwinCardNumber.First, true, popAction)
} else {
popAction()
}
},
)
}
}
private fun TwinCardNumber?.indexNumber(): String {
return this?.number?.toString() ?: ""
}
private fun TwinCardNumber?.pairIndexNumber(): String {
return this?.pairNumber()?.number?.toString() ?: ""
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.features.onboarding.products.twins.ui.dialog
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object TwinningProcessNotCompletedDialog {
fun create(context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog)
.setMessage(R.string.onboarding_twin_exit_warning)
.setPositiveButton(R.string.warning_button_ok) { _, _ -> }
.setOnDismissListener { store.dispatchDialogHide() }
.create()
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
sealed class BackupDialog : StateDialog {
data class UnfinishedBackupFound(
val scanResponse: ScanResponse? = null,
) : BackupDialog()
data class ConfirmDiscardingBackup(
val scanResponse: ScanResponse? = null,
) : BackupDialog()
}

View file

@ -0,0 +1,59 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.Onboarding.*
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
class BackupMiddleware {
val backupMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
if (action is BackupAction) handleBackupAction(state, action)
next(action)
}
}
}
}
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) {
if (DemoHelper.tryHandle(appState, action)) return
when (action) {
is BackupAction.DiscardBackup -> {
backupService.discardSavedBackup()
}
is BackupAction.DiscardSavedBackup -> {
mainScope.launch {
backupService.discardSavedBackup()
val onboardingRepository = store.inject(DaggerGraphState::onboardingRepository)
val cardRepository = store.inject(DaggerGraphState::cardRepository)
val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
cardRepository.finishCardActivation(unfinishedBackup.card.cardId)
onboardingRepository.clearUnfinishedFinalizeOnboarding()
Analytics.send(Finished())
}
}
is BackupAction.ResumeFoundUnfinishedBackup -> {
if (action.unfinishedBackupScanResponse != null) {
store.dispatchNavigationAction {
push(
AppRoute.Onboarding(
scanResponse = action.unfinishedBackupScanResponse,
mode = AppRoute.Onboarding.Mode.ContinueFinalize,
),
)
}
}
}
}
}

View file

@ -1,108 +1,19 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.net.Uri
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
data object Init : OnboardingWalletAction()
data object GetToCreateWalletStep : OnboardingWalletAction()
data object CreateWallet : OnboardingWalletAction()
data class WalletWasCreated(
val shouldSendAnalyticsEvent: Boolean,
val result: CompletionResult<CreateProductWalletTaskResponse>,
) : OnboardingWalletAction()
data object Done : OnboardingWalletAction()
data class FinishOnboarding(val scope: CoroutineScope) : OnboardingWalletAction()
data object ResumeBackup : OnboardingWalletAction()
data class LoadArtwork(val cardArtworkUriForUnfinishedBackup: Uri? = null) : OnboardingWalletAction()
class SetPrimaryCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
class SetSecondCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
class SetThirdCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
data object OnBackPressed : OnboardingWalletAction()
data class WalletSaved(val userWalletId: UserWalletId) : OnboardingWalletAction()
}
sealed class OnboardingWallet2Action : OnboardingWalletAction() {
data class Init(val maxProgress: Int) : OnboardingWallet2Action()
data class SetDependencies(val maxProgress: Int) : OnboardingWallet2Action()
data class CreateWallet(
val callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) : OnboardingWallet2Action()
data class ImportWallet(
val mnemonicComponents: List<String>,
val passphrase: String?,
val seedPhraseSource: SeedPhraseSource,
val callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) : OnboardingWallet2Action()
data class WalletWasCreated(
val result: CompletionResult<CreateWalletResponse>,
) : OnboardingWallet2Action()
}
sealed class BackupAction : Action {
data object IntroduceBackup : BackupAction()
data object StartBackup : BackupAction()
data object SkipBackup : BackupAction()
data object ErrorInBackupCard : BackupAction()
data object StartAddingPrimaryCard : BackupAction()
data object ScanPrimaryCard : BackupAction()
data class SetHasRing(val hasRing: Boolean) : BackupAction()
/**
* Check for unfinished backup of standard Wallets
* See more GlobalAction.Onboarding.StartForUnfinishedBackup
*/
data object CheckForUnfinishedBackup : BackupAction()
data object StartAddingBackupCards : BackupAction()
data object AddBackupCard : BackupAction() {
data class Success(val card: Card) : BackupAction()
data class ChangeButtonLoading(val isLoading: Boolean) : BackupAction()
}
data object FinishAddingBackupCards : BackupAction()
data object ShowAccessCodeInfoScreen : BackupAction()
data object ShowEnterAccessCodeScreen : BackupAction()
data class CheckAccessCode(val accessCode: String) : BackupAction()
data class SetAccessCodeError(val error: AccessCodeError?) : BackupAction()
data class SaveFirstAccessCode(val accessCode: String) : BackupAction()
data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction()
data object OnAccessCodeDialogClosed : BackupAction()
data object PrepareToWritePrimaryCard : BackupAction()
data object WritePrimaryCard : BackupAction()
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction()
data class BackupFinished(val userWalletId: UserWalletId?) : BackupAction()
data object DiscardBackup : BackupAction()
data object DiscardSavedBackup : BackupAction()
data class ResumeFoundUnfinishedBackup(
val unfinishedBackupScanResponse: ScanResponse?,
) : BackupAction()
data class ResetBackupCard(val cardId: String) : BackupAction()
}

View file

@ -1,849 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.net.Uri
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ifNotNull
import com.tangem.common.extensions.toHexString
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.Artwork
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.wallet.R
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
object OnboardingWalletMiddleware {
val handler = onboardingWalletMiddleware
}
private const val HIDE_PROGRESS_DELAY = 400L
private val onboardingWalletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is OnboardingWallet2Action -> handleWallet2Action(action)
else -> handleWalletAction(action)
}
next(action)
}
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleWalletAction(action: Action) {
if (action !is OnboardingWalletAction) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager
val scanResponse = onboardingManager?.scanResponse
val card = scanResponse?.card
val onboardingWalletState = store.state.onboardingWalletState
when (action) {
OnboardingWalletAction.Init -> {
ifNotNull(onboardingManager, card) { manager, notNullCard ->
mainScope.launch {
if (!manager.isActivationStarted(notNullCard.cardId)) {
Analytics.send(Onboarding.Started())
}
}
}
when {
card == null -> {
// it's possible when found unfinished backup
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> {
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
card.wallets.isNotEmpty() && card.backupStatus?.isActive == true -> {
store.dispatch(BackupAction.FinishBackup())
}
else -> {
store.dispatch(OnboardingWalletAction.GetToCreateWalletStep)
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
}
}
}
is OnboardingWalletAction.LoadArtwork -> {
scope.launch {
val cardArtwork = when (onboardingManager) {
null -> {
// onboardingManager is null when backup started from previously interrupted
val primaryCardId = backupService.primaryCardId
val cardPublicKey = backupService.primaryPublicKey
if (primaryCardId != null && cardPublicKey != null) {
// uses when no scanResponse and backup state restored
loadArtworkForCard(
cardId = primaryCardId,
cardPublicKey = cardPublicKey,
defaultArtwork = action.cardArtworkUriForUnfinishedBackup,
)
} else {
action.cardArtworkUriForUnfinishedBackup
}
}
else -> onboardingManager.loadArtworkUrl()
.takeIf { it != Artwork.DEFAULT_IMG_URL }
?.let { Uri.parse(it) }
}
store.dispatchOnMain(OnboardingWalletAction.SetPrimaryCardArtworkUrl(cardArtwork))
}
}
is OnboardingWalletAction.CreateWallet -> {
scanResponse ?: return
scope.launch {
val result = tangemSdkManager.createProductWallet(
scanResponse,
globalState.onboardingState.shouldResetOnCreate,
)
store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(true, result))
}
}
is OnboardingWalletAction.WalletWasCreated -> {
scanResponse ?: return
when (val result = action.result) {
is CompletionResult.Success -> {
if (action.shouldSendAnalyticsEvent) {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
}
// here we must use updated scanResponse after createWallet & derivation
val updatedResponse = globalState.onboardingState.onboardingManager.scanResponse.copy(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
)
onboardingManager.scanResponse = updatedResponse
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false))
mainScope.launch {
onboardingManager.startActivation(updatedResponse.card.cardId)
store.dispatchWithMain(OnboardingWalletAction.ResumeBackup)
}
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
handleActivationError()
}
}
}
}
is OnboardingWalletAction.FinishOnboarding -> {
store.dispatch(GlobalAction.Onboarding.Stop)
navigateToWalletScreen()
store.dispatch(BackupAction.DiscardBackup)
}
is OnboardingWalletAction.ResumeBackup -> {
val newAction = when (val backupState = backupService.currentState) {
BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard
is BackupService.State.FinalizingBackupCard -> BackupAction.PrepareToWriteBackupCard(backupState.index)
else -> {
if (onboardingWalletState.backupState.backupStep == null ||
onboardingWalletState.backupState.backupStep == BackupStep.InitBackup ||
onboardingWalletState.backupState.backupStep == BackupStep.Finished
) {
Analytics.send(Onboarding.Backup.ScreenOpened())
val isWallet2 = scanResponse?.cardTypesResolver?.isWallet2() ?: false
if (isWallet2) BackupAction.StartBackup else BackupAction.IntroduceBackup
} else {
null
}
}
}
newAction?.let { store.dispatch(it) }
}
OnboardingWalletAction.OnBackPressed -> handleOnBackPressed(onboardingWalletState)
else -> Unit
}
}
private suspend fun handleFinishBackup(scanResponse: ScanResponse, userWallet: UserWallet? = null) {
val backupState = store.state.onboardingWalletState.backupState
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
val userWalletId = userWallet?.walletId ?: UserWalletIdBuilder.scanResponse(scanResponse).build()
if (backupState.hasRing && userWalletId != null) {
scope.launch {
store.inject(DaggerGraphState::walletsRepository).setHasWalletsWithRing(userWalletId = userWalletId)
}
}
OnboardingHelper.saveWallet(
alreadyCreatedWallet = userWallet,
scanResponse = updatedScanResponse,
accessCode = backupState.accessCode,
backupCardsIds = backupState.backupCardIds,
hasBackupError = backupState.hasBackupError,
)
}
private fun navigateToWalletScreen() {
mainScope.launch {
val settingsRepository = store.inject(DaggerGraphState::settingsRepository)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
delay(timeMillis = 1_800)
store.dispatchNavigationAction { push(AppRoute.SaveWallet) }
}
}
}
private suspend fun readCard(): CompletionResult<ScanResponse> {
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes,
)
return store.inject(DaggerGraphState::scanCardProcessor).scan(analyticsSource = ScreensSources.Intro)
}
private suspend fun loadArtworkForCard(cardId: String, cardPublicKey: ByteArray, defaultArtwork: Uri?): Uri {
val onlineCardVerifier = store.inject(DaggerGraphState::onlineCardVerifier)
return when (val cardInfo = onlineCardVerifier.getCardInfo(cardId, cardPublicKey)) {
is Result.Success -> {
val artworkId = cardInfo.data.artwork?.id
if (artworkId.isNullOrEmpty()) {
defaultArtwork ?: Uri.EMPTY
} else {
Uri.parse(CardArtworksProvider.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId))
}
}
is Result.Failure -> defaultArtwork ?: Uri.EMPTY
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleWallet2Action(action: OnboardingWallet2Action) {
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager
val scanResponse = onboardingManager?.scanResponse
when (action) {
is OnboardingWallet2Action.Init -> {
if (scanResponse?.cardTypesResolver?.isWallet2() == true) {
store.dispatch(OnboardingWallet2Action.SetDependencies(action.maxProgress))
}
}
is OnboardingWallet2Action.CreateWallet -> {
scanResponse ?: return
scope.launch {
val mediateResult = when (
val result = tangemSdkManager.createProductWallet(
scanResponse,
globalState.onboardingState.shouldResetOnCreate,
)
) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false))
val response = CreateWalletResponse(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
)
CompletionResult.Success(response)
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
handleActivationError()
}
CompletionResult.Failure(result.error)
}
}
withMainContext {
action.callback(mediateResult)
}
}
}
is OnboardingWallet2Action.ImportWallet -> {
scanResponse ?: return
scope.launch {
val mediateResult = when (
val result = tangemSdkManager.importWallet(
scanResponse = scanResponse,
passphrase = action.passphrase,
mnemonic = action.mnemonicComponents.joinToString(" "),
shouldReset = globalState.onboardingState.shouldResetOnCreate,
)
) {
is CompletionResult.Success -> {
val creationType = when (action.seedPhraseSource) {
SeedPhraseSource.IMPORTED -> AnalyticsParam.WalletCreationType.SeedImport
SeedPhraseSource.GENERATED -> AnalyticsParam.WalletCreationType.NewSeed
}
Analytics.send(
event = Onboarding.CreateWallet.WalletCreatedSuccessfully(
creationType = creationType,
seedPhraseLength = action.mnemonicComponents.size,
),
)
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false))
val response = CreateWalletResponse(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
)
CompletionResult.Success(response)
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
handleActivationError()
}
CompletionResult.Failure(result.error)
}
}
withMainContext {
action.callback(mediateResult)
}
}
}
is OnboardingWallet2Action.WalletWasCreated -> {
val result = when (val mediateResult = action.result) {
is CompletionResult.Success -> {
val response = CreateProductWalletTaskResponse(
card = mediateResult.data.card,
derivedKeys = mediateResult.data.derivedKeys,
primaryCard = mediateResult.data.primaryCard,
)
CompletionResult.Success(response)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(mediateResult.error)
}
}
// do not send analytics event for wallet2 flow, cause its already sent
store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(false, result))
}
else -> Unit
}
}
private fun handleActivationError() {
store.dispatchDialogShow(
OnboardingDialog.WalletActivationError(
onConfirm = {
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(true))
},
),
)
}
private fun updateScanResponseAfterBackup(scanResponse: ScanResponse, backupState: BackupState): ScanResponse {
val card = if (backupState.backupCardsNumber > 0) {
val cardsCount = backupState.backupCardsNumber
scanResponse.card.copy(
backupStatus = CardDTO.BackupStatus.Active(cardCount = cardsCount),
isAccessCodeSet = true,
)
} else {
scanResponse.card
}
return scanResponse.copy(card = card)
}
class BackupMiddleware {
val backupMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
if (action is BackupAction) handleBackupAction(state, action)
next(action)
}
}
}
}
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) {
if (DemoHelper.tryHandle(appState, action)) return
val globalState = appState()?.globalState ?: return
val onboardingWalletState = appState()?.onboardingWalletState ?: return
val backupState = onboardingWalletState.backupState
var scanResponse = globalState.onboardingState.onboardingManager?.scanResponse
val card = scanResponse?.card
when (action) {
is BackupAction.StartBackup -> {
Analytics.send(Onboarding.Backup.Started())
backupService.discardSavedBackup()
val primaryCard = scanResponse?.primaryCard
if (primaryCard != null) {
backupService.setPrimaryCard(primaryCard)
store.dispatch(BackupAction.StartAddingBackupCards)
} else {
store.dispatch(BackupAction.StartAddingPrimaryCard)
}
}
is BackupAction.ScanPrimaryCard -> {
val iconScanRes = if (scanResponse?.cardTypesResolver?.isRing() == true) {
R.drawable.img_hand_scan_ring
} else {
null
}
backupService.readPrimaryCard(iconScanRes = iconScanRes, cardId = card?.cardId) { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.StartAddingBackupCards)
}
is CompletionResult.Failure -> Unit
}
}
}
is BackupAction.AddBackupCard -> {
store.dispatchOnMain(BackupAction.AddBackupCard.ChangeButtonLoading(true))
backupService.addBackupCard { result ->
backupService.skipCompatibilityChecks = false
store.inject(DaggerGraphState::cardSdkConfigRepository).sdk.config.filter.cardIdFilter = null
store.dispatchOnMain(BackupAction.AddBackupCard.ChangeButtonLoading(false))
when (result) {
is CompletionResult.Success -> {
updateArtworks(backupService.addedBackupCardsCount, result.data)
store.dispatchOnMain(BackupAction.AddBackupCard.Success(result.data))
}
is CompletionResult.Failure -> {
when (val error = result.error) {
is TangemSdkError.CardVerificationFailed -> {
Analytics.send(
event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed(
ScreensSources.Backup,
),
)
val resource = error.localizedDescriptionRes()
val resId = resource.resId ?: com.tangem.core.ui.R.string.common_unknown_error
val resArgs = resource.args.map { it.value }
store.inject(DaggerGraphState::uiMessageSender).send(
message = Dialogs.cardVerificationFailed(
errorDescription = resourceReference(id = resId, resArgs.toWrappedList()),
onRequestSupport = {
mainScope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.CardAttestationFailed)
}
},
),
)
}
is TangemSdkError.BackupFailedNotEmptyWallets -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
BackupDialog.ResetBackupCard(error.cardId),
),
)
}
is TangemSdkError.IssuerSignatureLoadingFailed -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(BackupDialog.AttestationFailed),
)
}
else -> Analytics.sendException(ExceptionAnalyticsEvent(error))
}
}
}
}
}
is BackupAction.FinishAddingBackupCards -> {
if (backupService.addedBackupCardsCount == backupState.maxBackupCards) {
store.dispatchOnMain(BackupAction.ShowAccessCodeInfoScreen)
} else {
store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards))
}
}
is BackupAction.ShowEnterAccessCodeScreen -> {
Analytics.send(Onboarding.Backup.SettingAccessCodeStarted())
}
is BackupAction.CheckAccessCode -> {
Analytics.send(Onboarding.Backup.AccessCodeEntered())
if (action.accessCode.length < 4) {
store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodeTooShort))
} else {
store.dispatch(BackupAction.SetAccessCodeError(null))
store.dispatch(BackupAction.SaveFirstAccessCode(action.accessCode))
}
}
is BackupAction.SaveAccessCodeConfirmation -> {
Analytics.send(Onboarding.Backup.AccessCodeReEntered())
if (action.accessCodeConfirmation == backupState.accessCode) {
store.dispatch(BackupAction.SetAccessCodeError(null))
backupService.setAccessCode(action.accessCodeConfirmation)
store.dispatch(BackupAction.PrepareToWritePrimaryCard)
} else {
store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodesDoNotMatch))
}
}
is BackupAction.WritePrimaryCard -> {
val isRing = scanResponse?.cardTypesResolver?.isRing() == true
val iconScanRes = if (isRing) R.drawable.img_hand_scan_ring else null
store.dispatchOnMain(BackupAction.SetHasRing(hasRing = isRing))
tangemSdkManager.changeProductType(isRing)
backupService.proceedBackup(iconScanRes = iconScanRes) { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(1))
}
is CompletionResult.Failure -> Unit
}
tangemSdkManager.clearProductType()
}
}
is BackupAction.WriteBackupCard -> {
val cardIndex = if (action.cardNumber > 0) action.cardNumber - 1 else action.cardNumber
val backupCard = backupState.backupCards.getOrNull(cardIndex)
val isRing = backupCard.isRing()
val iconScanRes = if (isRing) R.drawable.img_hand_scan_ring else null
store.dispatchOnMain(BackupAction.SetHasRing(hasRing = isRing))
tangemSdkManager.changeProductType(isRing)
backupService.proceedBackup(iconScanRes = iconScanRes) { result ->
when (result) {
is CompletionResult.Success -> {
val backupValidator = BackupValidator()
if (!backupValidator.isValidBackupStatus(CardDTO(result.data))) {
store.dispatchOnMain(BackupAction.ErrorInBackupCard)
}
if (backupService.currentState == BackupService.State.Finished) {
store.dispatchOnMain(BackupAction.FinishBackup())
} else {
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1))
}
}
is CompletionResult.Failure -> {
when (val error = result.error) {
is TangemSdkError.BackupFailedNotEmptyWallets -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
BackupDialog.ResetBackupCard(error.cardId),
),
)
}
}
}
}
tangemSdkManager.clearProductType()
}
}
is BackupAction.DiscardBackup -> {
backupService.discardSavedBackup()
}
is BackupAction.DiscardSavedBackup -> {
mainScope.launch {
backupService.primaryCardId?.let { // always null for onboarding v2
Analytics.send(Onboarding.Finished())
store.state.globalState.onboardingState.onboardingManager?.finishActivation(it)
}
backupService.discardSavedBackup()
val isOnboardingV2Enabled = store.inject(DaggerGraphState::onboardingV2FeatureToggles)
.isOnboardingV2Enabled
if (isOnboardingV2Enabled) {
val onboardingRepository = store.inject(DaggerGraphState::onboardingRepository)
val cardRepository = store.inject(DaggerGraphState::cardRepository)
val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
cardRepository.finishCardActivation(unfinishedBackup.card.cardId)
onboardingRepository.clearUnfinishedFinalizeOnboarding()
Analytics.send(Onboarding.Finished())
}
}
}
is BackupAction.CheckForUnfinishedBackup -> {
val isOnboardingV2Enabled = store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled
if (isOnboardingV2Enabled) {
val onboardingRepository = store.inject(DaggerGraphState::onboardingRepository)
mainScope.launch {
val onboardingScanResponse = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound(onboardingScanResponse)))
}
} else if (backupService.hasIncompletedBackup) {
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound()))
}
}
is BackupAction.ResumeFoundUnfinishedBackup -> {
if (action.unfinishedBackupScanResponse != null) {
// onboarding V2
store.dispatchNavigationAction {
push(
AppRoute.Onboarding(
scanResponse = action.unfinishedBackupScanResponse,
mode = AppRoute.Onboarding.Mode.ContinueFinalize,
),
)
}
} else {
store.dispatch(
GlobalAction.Onboarding.StartForUnfinishedBackup(
addedBackupCardsCount = backupService.addedBackupCardsCount,
),
)
store.dispatchNavigationAction { push(AppRoute.OnboardingWallet()) }
}
}
is BackupAction.SkipBackup -> {
Analytics.send(Onboarding.Backup.Skipped())
Analytics.send(Onboarding.Finished())
scope.launch {
launch {
store.state.globalState.onboardingState.onboardingManager?.finishActivation(
cardIds = gatherCardIds(backupState, card),
)
}
with(scanResponse) {
if (this == null) {
when (val result = readCard()) {
is CompletionResult.Success -> handleFinishBackup(result.data)
is CompletionResult.Failure -> store.dispatchNavigationAction(AppRouter::pop)
}
} else {
handleFinishBackup(scanResponse = this)
}
}
}
}
is BackupAction.FinishBackup -> {
scope.launch {
if (action.withAnalytics) {
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
}
var userWallet: UserWallet? = null
if (scanResponse != null) {
scanResponse = updateScanResponseAfterBackup(scanResponse!!, backupState)
userWallet = createUserWallet(
scanResponse = requireNotNull(value = scanResponse, lazyMessage = { "ScanResponse is null" }),
backupState = backupState,
)
} else {
delay(HIDE_PROGRESS_DELAY)
when (val result = readCard()) {
is CompletionResult.Failure -> {
store.dispatchNavigationAction(AppRouter::pop)
return@launch
}
is CompletionResult.Success -> {
scanResponse = result.data
userWallet = createUserWallet(scanResponse = result.data, backupState = backupState)
}
}
}
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
when (backupState.startedSource) {
BackupStartedSource.Onboarding -> saveWallet(
userWalletsListManager = userWalletsListManager,
userWallet = userWallet,
scanResponse = scanResponse,
backupState = backupState,
)
BackupStartedSource.CreateBackup -> updateWallet(
userWalletsListManager = userWalletsListManager,
userWallet = userWallet,
backupState = backupState,
)
}
scope.launch {
if (userWallet.scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported) {
store.inject(DaggerGraphState::walletsRepository).markWallet2WasCreated(userWallet.walletId)
}
}
val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull {
if (store.state.globalState.onboardingState.onboardingManager?.isActivationFinished(it) == true) {
null
} else {
it
}
}
// All cardIds may already be activated if the backup was skipped before.
if (notActivatedCardIds.isEmpty()) {
delay(1000)
store.dispatchWithMain(BackupAction.BackupFinished(userWallet.walletId))
return@launch
}
Analytics.send(Onboarding.Finished())
store.state.globalState.onboardingState.onboardingManager?.finishActivation(notActivatedCardIds)
handleFinishBackup(requireNotNull(scanResponse), userWallet)
store.dispatchWithMain(BackupAction.BackupFinished(userWalletId = userWallet.walletId))
}
}
is BackupAction.ResetBackupCard -> {
scope.launch { tangemSdkManager.resetToFactorySettings(action.cardId, false) }
}
else -> Unit
}
}
private fun Card?.isRing(): Boolean {
return this?.let { RING_BATCH_IDS.contains(batchId) || batchId.startsWith(RING_BATCH_PREFIX) } ?: false
}
private suspend fun saveWallet(
userWalletsListManager: UserWalletsListManager,
userWallet: UserWallet,
scanResponse: ScanResponse?,
backupState: BackupState,
) {
userWalletsListManager.save(
userWallet = userWallet.copy(
scanResponse = updateScanResponseAfterBackup(
scanResponse = requireNotNull(
value = scanResponse,
lazyMessage = { "ScanResponse is null" },
),
backupState = backupState,
),
),
canOverride = true,
)
}
private suspend fun updateWallet(
userWalletsListManager: UserWalletsListManager,
userWallet: UserWallet,
backupState: BackupState,
) {
userWalletsListManager.update(
userWalletId = userWallet.walletId,
update = { wallet ->
wallet.copy(
scanResponse = updateScanResponseAfterBackup(
scanResponse = wallet.scanResponse,
backupState = backupState,
),
)
},
)
}
private suspend fun createUserWallet(scanResponse: ScanResponse, backupState: BackupState): UserWallet {
val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse)
return requireNotNull(
value = userWalletBuilder
.backupCardsIds(backupState.backupCardIds.toSet())
.hasBackupError(backupState.hasBackupError)
.build(),
lazyMessage = { "User wallet not created" },
)
}
fun updateArtworks(addedBackupCardsCount: Int, card: Card) {
mainScope.launch {
withIOContext {
val imageUri = loadArtworkForCard(card.cardId, card.cardPublicKey, Uri.EMPTY)
when (addedBackupCardsCount) {
1 -> {
store.dispatchOnMain(OnboardingWalletAction.SetSecondCardArtworkUrl(imageUri))
}
2 -> {
store.dispatchOnMain(OnboardingWalletAction.SetThirdCardArtworkUrl(imageUri))
}
}
}
}
}
internal fun gatherCardIds(backupState: BackupState, card: CardDTO?): List<String> {
return (listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds)
.filterNotNull()
.distinct()
}
private fun handleOnBackPressed(state: OnboardingWalletState) {
when (state.backupState.backupStep) {
null, BackupStep.InitBackup, BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode,
BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard,
-> {
showInterruptOnboardingDialog()
}
is BackupStep.WriteBackupCard -> {
store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress))
}
BackupStep.Finished -> {
OnboardingHelper.onInterrupted()
store.dispatchNavigationAction(AppRouter::pop)
}
}
}
private fun showInterruptOnboardingDialog() {
store.dispatchDialogShow(
OnboardingDialog.InterruptOnboarding(
onOk = {
OnboardingHelper.onInterrupted()
store.dispatch(BackupAction.DiscardBackup)
store.dispatchNavigationAction(AppRouter::pop)
},
),
)
}

View file

@ -1,185 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.domain.redux.OnboardingManageTokensAction
import com.tangem.tap.backupService
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import org.rekotlin.Action
object OnboardingWalletReducer {
fun reduce(action: Action, state: AppState): OnboardingWalletState = internalReduce(action, state)
}
@Suppress("CyclomaticComplexMethod")
private fun internalReduce(action: Action, appState: AppState): OnboardingWalletState {
val state = appState.onboardingWalletState
val backupState = state.backupState
return when (action) {
is GlobalAction.Onboarding -> ReducerForGlobalAction.reduce(action, state)
is BackupAction.BackupFinished -> {
state.copy(
step = if (action.userWalletId != null && backupState.startedSource == BackupStartedSource.Onboarding) {
OnboardingWalletStep.ManageTokens
} else {
OnboardingWalletStep.Done
},
userWalletId = action.userWalletId,
)
}
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action = action, state = state.backupState))
is OnboardingWallet2Action -> OnboardingWallet2Reducer.reduce(action, state)
is OnboardingWalletAction.GetToCreateWalletStep -> state.copy(
step = OnboardingWalletStep.CreateWallet,
)
is OnboardingWalletAction.ResumeBackup -> state.copy(step = OnboardingWalletStep.Backup)
is OnboardingWalletAction.SetPrimaryCardArtworkUrl -> {
state.copy(
walletImages = state.walletImages.copy(primaryCardImage = action.artworkUri),
)
}
is OnboardingWalletAction.SetSecondCardArtworkUrl -> {
state.copy(
walletImages = state.walletImages.copy(secondCardImage = action.artworkUri),
)
}
is OnboardingWalletAction.SetThirdCardArtworkUrl -> {
state.copy(
walletImages = state.walletImages.copy(thirdCardImage = action.artworkUri),
)
}
is OnboardingManageTokensAction.CurrenciesSaved -> state.copy(step = OnboardingWalletStep.Done)
is OnboardingWalletAction.WalletSaved -> state.copy(
step = when (backupState.startedSource) {
BackupStartedSource.Onboarding -> OnboardingWalletStep.ManageTokens
BackupStartedSource.CreateBackup -> OnboardingWalletStep.Done
},
userWalletId = action.userWalletId,
)
is OnboardingWalletAction.Done -> state.copy(step = OnboardingWalletStep.Done)
else -> state
}
}
private object ReducerForGlobalAction {
private const val MAX_BACKUP_CARDS = 2
fun reduce(action: GlobalAction.Onboarding, state: OnboardingWalletState): OnboardingWalletState {
return when (action) {
is GlobalAction.Onboarding.Start -> {
OnboardingWalletState(
backupState = state.backupState.copy(
maxBackupCards = MAX_BACKUP_CARDS,
canSkipBackup = action.canSkipBackup,
startedSource = action.source,
),
)
}
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
OnboardingWalletState(
backupState = state.backupState.copy(
maxBackupCards = action.addedBackupCardsCount,
canSkipBackup = false,
isInterruptedBackup = true,
),
)
}
else -> state
}
}
}
private object OnboardingWallet2Reducer {
fun reduce(action: OnboardingWallet2Action, state: OnboardingWalletState): OnboardingWalletState = when (action) {
is OnboardingWallet2Action.SetDependencies -> state.copy(
wallet2State = OnboardingWallet2State(
maxProgress = action.maxProgress,
),
)
else -> state
}
}
private object BackupReducer {
@Suppress("ComplexMethod", "LongMethod")
fun reduce(action: BackupAction, state: BackupState): BackupState {
return when (action) {
is BackupAction.IntroduceBackup -> BackupState(
backupStep = BackupStep.InitBackup,
maxBackupCards = 2,
canSkipBackup = state.canSkipBackup,
startedSource = state.startedSource,
)
BackupAction.StartAddingPrimaryCard -> {
state.copy(
backupStep = BackupStep.ScanOriginCard,
maxBackupCards = 2,
canSkipBackup = state.canSkipBackup,
)
}
BackupAction.StartAddingBackupCards -> {
state.copy(
backupStep = BackupStep.AddBackupCards,
maxBackupCards = 2,
canSkipBackup = state.canSkipBackup,
)
}
is BackupAction.AddBackupCard.Success -> {
state.copy(
backupCards = state.backupCards + action.card,
backupCardsNumber = state.backupCardsNumber + 1,
)
}
is BackupAction.AddBackupCard.ChangeButtonLoading -> state.copy(showBtnLoading = action.isLoading)
BackupAction.ShowAccessCodeInfoScreen -> state.copy(backupStep = BackupStep.SetAccessCode)
BackupAction.ShowEnterAccessCodeScreen -> state.copy(
backupStep = BackupStep.EnterAccessCode,
accessCodeError = null,
)
is BackupAction.SaveFirstAccessCode -> state.copy(
backupStep = BackupStep.ReenterAccessCode,
accessCode = action.accessCode,
)
is BackupAction.SetAccessCodeError -> state.copy(accessCodeError = action.error)
is BackupAction.PrepareToWritePrimaryCard -> if (state.primaryCardId == null) {
state.copy(
primaryCardId = backupService.primaryCardId,
primaryCardBatchId = backupService.primaryCardBatchId,
backupCardIds = backupService.backupCardIds,
backupCardBatchIds = backupService.backupCardsBatchIds,
backupCardsNumber = backupService.backupCardIds.size,
backupStep = BackupStep.WritePrimaryCard,
)
} else {
state.copy(backupStep = BackupStep.WritePrimaryCard)
}
is BackupAction.PrepareToWriteBackupCard -> if (state.primaryCardId == null) {
state.copy(
primaryCardId = backupService.primaryCardId,
primaryCardBatchId = backupService.primaryCardBatchId,
backupCardIds = backupService.backupCardIds,
backupCardBatchIds = backupService.backupCardsBatchIds,
backupCardsNumber = backupService.backupCardIds.size,
backupStep = BackupStep.WriteBackupCard(action.cardNumber),
)
} else {
state.copy(backupStep = BackupStep.WriteBackupCard(action.cardNumber))
}
is BackupAction.ErrorInBackupCard -> state.copy(hasBackupError = true)
is BackupAction.SkipBackup -> state.copy(backupStep = BackupStep.Finished)
is BackupAction.FinishBackup -> state.copy(backupStep = BackupStep.Finished)
BackupAction.OnAccessCodeDialogClosed -> state.copy(backupStep = BackupStep.AddBackupCards)
BackupAction.DiscardBackup -> BackupState()
is BackupAction.SetHasRing -> {
// Don't update if state.has ring is already true
if (state.hasRing) {
state
} else {
state.copy(hasRing = action.hasRing)
}
}
else -> state
}
}
}

View file

@ -1,128 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.graphics.Bitmap
import android.net.Uri
import com.tangem.common.card.Card
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.wallets.models.UserWalletId
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class OnboardingWalletState(
val step: OnboardingWalletStep = OnboardingWalletStep.None,
val wallet2State: OnboardingWallet2State? = null,
val backupState: BackupState = BackupState(),
val walletImages: WalletImages = WalletImages(),
val showConfetti: Boolean = false,
val isRingOnboarding: Boolean = false,
val userWalletId: UserWalletId? = null,
) : StateType {
@Suppress("MagicNumber")
fun getMaxProgress(): Int {
val baseProgress = 7
return getWallet2Progress() + baseProgress
}
@Suppress("ComplexMethod", "MagicNumber")
fun getProgressStep(): Int {
val progressByStep = when (step) {
OnboardingWalletStep.CreateWallet, OnboardingWalletStep.None -> 1
OnboardingWalletStep.Backup -> {
when (backupState.backupStep) {
null -> 2
BackupStep.InitBackup -> 2
BackupStep.ScanOriginCard -> 3
BackupStep.AddBackupCards -> 4
BackupStep.EnterAccessCode -> 4
BackupStep.ReenterAccessCode -> 4
BackupStep.SetAccessCode -> 4
BackupStep.WritePrimaryCard, is BackupStep.WriteBackupCard -> 5
BackupStep.Finished -> 6
}
}
OnboardingWalletStep.ManageTokens -> 6
OnboardingWalletStep.Done -> getMaxProgress()
}
return getWallet2Progress() + progressByStep
}
private fun getWallet2Progress(): Int = wallet2State?.maxProgress ?: 0
}
data class WalletImages(
val primaryCardImage: Uri? = null,
val secondCardImage: Uri? = null,
val thirdCardImage: Uri? = null,
)
data class OnboardingWallet2State(
val maxProgress: Int,
)
enum class OnboardingWalletStep {
None, CreateWallet, Backup, ManageTokens, Done
}
data class BackupState(
val primaryCardId: String? = null,
val primaryCardBatchId: String? = null,
val backupCardsNumber: Int = 0,
val backupCards: List<Card> = emptyList(),
val backupCardIds: List<CardId> = emptyList(),
val backupCardBatchIds: List<CardId> = emptyList(),
@Transient
val backupCardsArtworks: Map<CardId, Bitmap> = emptyMap(),
val accessCode: String = "",
val accessCodeError: AccessCodeError? = null,
val backupStep: BackupStep? = null,
val maxBackupCards: Int = 2,
val canSkipBackup: Boolean = true,
val isInterruptedBackup: Boolean = false,
val showBtnLoading: Boolean = false,
val hasBackupError: Boolean = false,
val startedSource: BackupStartedSource = BackupStartedSource.Onboarding,
val hasRing: Boolean = false,
)
enum class BackupStartedSource {
Onboarding, CreateBackup
}
enum class AccessCodeError {
CodeTooShort, CodesDoNotMatch
}
typealias CardId = String
sealed class BackupStep {
data object InitBackup : BackupStep()
data object ScanOriginCard : BackupStep()
data object AddBackupCards : BackupStep()
data object SetAccessCode : BackupStep()
data object EnterAccessCode : BackupStep()
data object ReenterAccessCode : BackupStep()
data object WritePrimaryCard : BackupStep()
data class WriteBackupCard(val cardNumber: Int) : BackupStep()
data object Finished : BackupStep()
}
sealed class BackupDialog : StateDialog {
data object AttestationFailed : BackupDialog()
data object AddMoreBackupCards : BackupDialog()
data object BackupInProgress : BackupDialog()
data class UnfinishedBackupFound(
val scanResponse: ScanResponse? = null,
) : BackupDialog()
data class ConfirmDiscardingBackup(
val scanResponse: ScanResponse? = null,
) : BackupDialog()
data class ResetBackupCard(val cardId: String) : BackupDialog()
}

View file

@ -1,314 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import com.google.android.material.button.MaterialButton
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.postUiDelayBg
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState
import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.*
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
/**
[REDACTED_AUTHOR]
*/
interface BackupAnimator {
fun updateBackupState(backupState: BackupState)
fun setupCreateWalletState()
fun showBackupIntro(state: BackupState)
fun showScanOriginCard()
fun showAddBackupCards(state: BackupState, backupCardsCounts: Int)
fun showWritePrimaryCard(state: BackupState)
fun showWriteBackupCard(state: BackupState, backupCard: Int)
fun showSuccess(onEnd: () -> Unit)
}
class WalletBackupAnimator(
private val cardsWidget: WalletCardsWidget,
) : BackupAnimator {
private val viewState: State = when (cardsWidget.leapfrogWidget.getViewsCount()) {
2 -> State.TwoCards
3 -> State.ThreeCards
else -> throw UnsupportedOperationException()
}
private var currentStep = -1
private var firstBackupCardAnimated = false
private var secondBackupCardAnimated = false
override fun updateBackupState(backupState: BackupState) {
}
override fun showBackupIntro(state: BackupState) {
currentStep = STEP_BACKUP_INTRO
ORIGIN.show()
FIRST_BACKUP.show()
SECOND_BACKUP.show()
cardsWidget.toWelcome()
}
override fun setupCreateWalletState() {
currentStep = STEP_CREATE_WALLET
cardsWidget.toFolded(false)
ORIGIN.alpha(1f)
FIRST_BACKUP.alpha(1f)
SECOND_BACKUP.alpha(1f)
}
override fun showScanOriginCard() {
currentStep = STEP_SCAN_ORIGIN_CARD
cardsWidget.toFolded()
}
@Suppress("MagicNumber")
override fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) {
when (backupCardsCounts) {
0 -> {
when (currentStep) {
UNDEFINED, STEP_CREATE_WALLET -> {
cardsWidget.toFolded(false) { cardsWidget.toFan() }
FIRST_BACKUP.animateAlpha(0.6f, 0)
SECOND_BACKUP.animateAlpha(0.2f, 400)
}
STEP_BACKUP_INTRO -> {
cardsWidget.toFolded(duration = 300) { cardsWidget.toFan() }
FIRST_BACKUP.animateAlpha(0.6f, 400, 250)
SECOND_BACKUP.animateAlpha(0.2f, 400, 250)
}
STEP_SCAN_ORIGIN_CARD -> {
cardsWidget.toFan()
FIRST_BACKUP.animateAlpha(0.6f, 400, 250)
SECOND_BACKUP.animateAlpha(0.2f, 400, 250)
}
}
}
1 -> {
cardsWidget.toFan(false)
FIRST_BACKUP.alpha(1f)
SECOND_BACKUP.alpha(0.2f)
}
2 -> {
cardsWidget.toFan(false)
FIRST_BACKUP.alpha(1f)
SECOND_BACKUP.alpha(1f)
}
}
currentStep = STEP_ADD_BACKUP_CARDS
}
@Suppress("MagicNumber")
override fun showWritePrimaryCard(state: BackupState) {
currentStep = STEP_WRITE_PRIMARY_CARD
cardsWidget.toLeapfrog(
onEndFold = {
FIRST_BACKUP.alpha(0.4f)
SECOND_BACKUP.alpha(0.2f)
},
)
}
@Suppress("MagicNumber")
override fun showWriteBackupCard(state: BackupState, backupCard: Int) {
when (viewState) {
State.TwoCards -> {
if (firstBackupCardAnimated) return
firstBackupCardAnimated = true
val delay = if (currentStep == UNDEFINED) 700L else 0
postUiDelayBg(delay) {
FIRST_BACKUP.alpha(1f)
cardsWidget.leapfrogWidget.leap {
ORIGIN.alpha(0.4f)
}
}
}
State.ThreeCards -> {
when (backupCard) {
1 -> {
if (firstBackupCardAnimated) return
firstBackupCardAnimated = true
FIRST_BACKUP.alpha(1f)
cardsWidget.leapfrogWidget.leap {
SECOND_BACKUP.alpha(0.4f)
ORIGIN.alpha(0.2f)
}
}
2 -> {
if (secondBackupCardAnimated) return
secondBackupCardAnimated = true
/*
* !!! Workaround !!!
* For interrupted backup
* */
if (!firstBackupCardAnimated) {
showWriteBackupCard(state, backupCard = 1)
return
}
SECOND_BACKUP.alpha(1f)
cardsWidget.leapfrogWidget.leap {
ORIGIN.alpha(0.4f)
FIRST_BACKUP.alpha(0.2f)
}
}
}
}
}
currentStep = STEP_WRITE_BACKUP_CARD
}
override fun showSuccess(onEnd: () -> Unit) {
currentStep = STEP_SUCCESS
cardsWidget.leapfrogWidget.fold(onEnd = onEnd)
}
private enum class State {
ThreeCards,
TwoCards,
}
private fun BackupCardType.animateAlpha(alpha: Float, duration: Long = 0L, startDelay: Long = 0L) {
when (this) {
ORIGIN -> {
cardsWidget.getOriginCardView().animate().apply {
alpha(alpha)
this.startDelay = startDelay
this.duration = duration
}
}
FIRST_BACKUP -> {
cardsWidget.getFirstBackupCardView().animate().apply {
alpha(alpha)
this.startDelay = startDelay
this.duration = duration
}
}
SECOND_BACKUP -> {
if (viewState != State.ThreeCards) return
cardsWidget.getSecondBackupCardView().animate().apply {
alpha(alpha)
this.startDelay = startDelay
this.duration = duration
}
}
}
}
private fun BackupCardType.alpha(alpha: Float) {
when (this) {
ORIGIN -> cardsWidget.getOriginCardView().alpha = alpha
FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().alpha = alpha
SECOND_BACKUP -> {
if (viewState != State.ThreeCards) return
cardsWidget.getSecondBackupCardView().alpha = alpha
}
}
}
private fun BackupCardType.show() {
when (this) {
ORIGIN -> cardsWidget.getOriginCardView().show()
FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().show()
SECOND_BACKUP -> {
if (viewState != State.ThreeCards) return
cardsWidget.getSecondBackupCardView().show()
}
}
}
companion object {
private const val UNDEFINED = -1
private const val STEP_CREATE_WALLET = 0
private const val STEP_BACKUP_INTRO = 1
private const val STEP_SCAN_ORIGIN_CARD = 2
private const val STEP_ADD_BACKUP_CARDS = 3
private const val STEP_WRITE_PRIMARY_CARD = 4
private const val STEP_WRITE_BACKUP_CARD = 5
private const val STEP_SUCCESS = 6
}
}
class TestBackupAnimation(
private val animator: BackupAnimator,
binding: FragmentOnboardingWalletBinding,
) : BackupAnimator {
private lateinit var state: BackupState
private var steps = 0
init {
init(binding, this)
}
@Suppress("MagicNumber")
private fun setStep(step: Int, onStepUpdate: (Int) -> Unit = {}) {
steps = step
when (steps) {
0 -> setupCreateWalletState()
1 -> showBackupIntro(state)
2 -> showScanOriginCard()
3 -> showAddBackupCards(state, 0)
4 -> showAddBackupCards(state, 1)
5 -> showAddBackupCards(state, 2)
6 -> showWritePrimaryCard(state)
7 -> showWriteBackupCard(state, 1)
8 -> showWriteBackupCard(state, 2)
else -> setStep(0)
}
onStepUpdate(steps)
}
fun onNext(onStepUpdate: (Int) -> Unit) {
steps++
setStep(steps, onStepUpdate)
}
override fun updateBackupState(backupState: BackupState) {
this.state = backupState
}
override fun setupCreateWalletState() {
animator.setupCreateWalletState()
}
override fun showBackupIntro(state: BackupState) {
animator.showBackupIntro(state)
}
override fun showScanOriginCard() {
animator.showScanOriginCard()
}
override fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) {
animator.showAddBackupCards(state, backupCardsCounts)
}
override fun showWritePrimaryCard(state: BackupState) {
animator.showWritePrimaryCard(state)
}
override fun showWriteBackupCard(state: BackupState, backupCard: Int) {
animator.showWriteBackupCard(state, backupCard)
}
override fun showSuccess(onEnd: () -> Unit) {
animator.showSuccess(onEnd)
}
companion object {
fun init(binding: FragmentOnboardingWalletBinding, animator: TestBackupAnimation) {
val button = MaterialButton(binding.onboardingRoot.context).apply { text = "Next" }
button.setOnClickListener { animator.onNext { button.text = "$it" } }
binding.onboardingRoot.addView(button, 0)
}
}
}

View file

@ -1,57 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import com.tangem.tap.common.extensions.getString
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ItemBackupInfoAdapterBinding
class BackupInfoAdapter : RecyclerView.Adapter<BackupInfoViewHolder>() {
private val data = backupInfoSnippets
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BackupInfoViewHolder = BackupInfoViewHolder(
ItemBackupInfoAdapterBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false,
),
)
override fun getItemCount(): Int = data.size
override fun onBindViewHolder(holder: BackupInfoViewHolder, position: Int) = holder.binding.run {
tvHeader.text = root.getString(data[position].header)
tvBody.text = root.getString(data[position].body)
}
}
private class BackupInfo(
@StringRes val header: Int,
@StringRes val body: Int,
)
private val backupInfoSnippets = listOf(
BackupInfo(
R.string.onboarding_wallet_info_title_first,
R.string.onboarding_wallet_info_subtitle_first,
),
BackupInfo(
R.string.onboarding_wallet_info_title_second,
R.string.onboarding_wallet_info_subtitle_second,
),
BackupInfo(
R.string.onboarding_wallet_info_title_third,
R.string.onboarding_wallet_info_subtitle_third,
),
BackupInfo(
R.string.onboarding_wallet_info_title_fourth,
R.string.onboarding_wallet_info_subtitle_fourth,
),
)
class BackupInfoViewHolder(val binding: ItemBackupInfoAdapterBinding) :
RecyclerView.ViewHolder(binding.root)

View file

@ -1,82 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.collectAsState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.windowsize.rememberWindowSize
import com.tangem.feature.onboarding.api.OnboardingSeedPhraseScreen
import com.tangem.feature.onboarding.api.OnboardingSeedPhraseApi
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
internal class OnboardingSeedPhraseStateHandler(
private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhraseScreen(),
private val activity: Activity,
) {
fun newState(
walletFragment: OnboardingWalletFragment,
state: OnboardingWalletState,
seedPhraseViewModel: SeedPhraseViewModel,
) {
if (state.step == OnboardingWalletStep.CreateWallet && !seedPhraseViewModel.isFinished) {
switchToWallet2SeedPhraseOnboarding(walletFragment, seedPhraseViewModel, state.getMaxProgress())
} else {
switchToWalletOnboarding(walletFragment, state)
}
}
private fun switchToWalletOnboarding(walletFragment: OnboardingWalletFragment, state: OnboardingWalletState) {
walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.hide()
walletFragment.pbBinding.pbState.show()
walletFragment.binding.onboardingWalletContainer.show()
walletFragment.handleOnboardingStep(state)
}
private fun switchToWallet2SeedPhraseOnboarding(
walletFragment: OnboardingWalletFragment,
viewModel: SeedPhraseViewModel,
onboardingWalletMaxProgress: Int,
) {
walletFragment.pbBinding.pbState.hide()
walletFragment.binding.onboardingWalletContainer.hide()
walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.show()
walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.setContent {
val subScreen = viewModel.currentScreen.collectAsState().value
setMainScreenToolbarTitle(walletFragment, subScreen)
TangemTheme(
isDark = isSystemInDarkTheme(),
windowSize = rememberWindowSize(activity = activity),
) {
onboardingSeedPhraseApi.ScreenContent(
uiState = viewModel.uiState,
subScreen = subScreen,
progress = viewModel.progress.collectAsState(0).value.toFloat() / onboardingWalletMaxProgress,
)
}
}
}
private fun setMainScreenToolbarTitle(walletFragment: OnboardingWalletFragment, subScreen: SeedPhraseScreen) {
val titleResId = when (subScreen) {
SeedPhraseScreen.Intro -> R.string.wallet_title
SeedPhraseScreen.AboutSeedPhrase,
SeedPhraseScreen.YourSeedPhrase,
SeedPhraseScreen.CheckSeedPhrase,
-> R.string.onboarding_create_wallet_header
SeedPhraseScreen.ImportSeedPhrase -> R.string.onboarding_seed_intro_button_import
}
walletFragment.binding.toolbar.title = walletFragment.getString(titleResId)
}
}

View file

@ -1,691 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.content.res.ColorStateList
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.core.view.MenuProvider
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.transition.TransitionManager
import by.kirich1409.viewbindingdelegate.viewBinding
import coil.load
import com.arkivanov.decompose.defaultComponentContext
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayoutMediator
import com.tangem.common.CardIdFormatter
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.windowsize.rememberWindowSize
import com.tangem.datasource.utils.isNullOrEmpty
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.isRing
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseMediator
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseRouter
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.sdk.ui.widget.leapfrogWidget.PropertyCalculator
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.*
import com.tangem.tap.features.BaseFragment
import com.tangem.tap.features.FragmentOnBackPressedHandler
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.OnboardingMenuProvider
import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
import com.tangem.wallet.databinding.LayoutOnboardingSeedPhraseBinding
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
import timber.log.Timber
import javax.inject.Inject
@Suppress("LargeClass", "MagicNumber")
@AndroidEntryPoint
class OnboardingWalletFragment :
BaseFragment(R.layout.fragment_onboarding_wallet),
StoreSubscriber<OnboardingWalletState>,
FragmentOnBackPressedHandler {
@Inject
internal lateinit var uiDependencies: UiDependencies
@Inject
internal lateinit var onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory
@Inject
@RootAppComponentContext
internal lateinit var rootComponentContext: AppComponentContext
private var onboardingManageTokensComponent: OnboardingManageTokensComponent? = null
private lateinit var onboardingComponentContext: AppComponentContext
internal val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind)
internal val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
internal val bindingSeedPhrase: LayoutOnboardingSeedPhraseBinding by lazy { binding.onboardingSeedPhraseContainer }
private val bindingManageTokens by lazy { binding.onboardingManageTokensContainer }
private val canSkipBackup by lazy { arguments?.getBoolean(AppRoute.OnboardingWallet.CAN_SKIP_BACKUP_KEY) ?: true }
private lateinit var seedPhraseStateHandler: OnboardingSeedPhraseStateHandler
private val seedPhraseViewModel by viewModels<SeedPhraseViewModel>()
private lateinit var cardsWidget: WalletCardsWidget
private var seedPhraseRouter: SeedPhraseRouter? = null
private var accessCodeDialog: AccessCodeDialog? = null
private lateinit var animator: BackupAnimator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
seedPhraseStateHandler = OnboardingSeedPhraseStateHandler(activity = requireActivity())
val newSeedPhraseRouter = makeSeedPhraseRouter()
seedPhraseRouter = newSeedPhraseRouter
seedPhraseViewModel.setRouter(newSeedPhraseRouter)
seedPhraseViewModel.setMediator(makeSeedPhraseMediator())
onboardingComponentContext = rootComponentContext.childByContext(
componentContext = defaultComponentContext(onBackPressedDispatcher = null),
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val scaleFactor = getDeviceScaleFactor()
initCardsWidget(createLeapfrogWidget(binding.flCardsContainer, scaleFactor), scaleFactor)
binding.viewPagerBackupInfo.adapter = BackupInfoAdapter()
TabLayoutMediator(
binding.tabLayoutBackupInfo,
binding.viewPagerBackupInfo,
) { tab, position ->
// Some implementation
}.attach()
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
addBackPressHandler(handler = this)
store.dispatch(OnboardingWallet2Action.Init(seedPhraseViewModel.maxProgress))
store.dispatch(OnboardingWalletAction.Init)
store.dispatch(
OnboardingWalletAction.LoadArtwork(
cardArtworkUriForUnfinishedBackup = requireContext().resourceUri(R.drawable.card_placeholder_wallet),
),
)
}
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider(
scanResponseProvider = Provider {
store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: error("ScanResponse must be not null")
},
)
private fun reInitCardsWidgetIfNeeded(backupCardsCounts: Int) = with(binding) {
val viewBackupCount = flCardsContainer.childCount - 1
if (viewBackupCount <= 0) return@with
if (viewBackupCount == backupCardsCounts) return@with
cardsWidget.toFolded(false)
if (viewBackupCount > backupCardsCounts) {
flCardsContainer.removeViews(0, viewBackupCount - backupCardsCounts)
} else {
flCardsContainer.inflate(R.layout.view_onboarding_card, true)
}
val scaleFactor = getDeviceScaleFactor()
initCardsWidget(createLeapfrogWidget(flCardsContainer, scaleFactor), scaleFactor)
}
private fun createLeapfrogWidget(container: FrameLayout, deviceScaleFactor: Float): LeapfrogWidget {
val leapfrogCalculator = PropertyCalculator(
yTranslationFactor = when (container.childCount) {
3 -> 25f
else -> 35f
} * deviceScaleFactor,
)
return LeapfrogWidget(container, leapfrogCalculator)
}
private fun initCardsWidget(leapfrogWidget: LeapfrogWidget, deviceScaleFactor: Float, isTest: Boolean = false) {
cardsWidget = WalletCardsWidget(leapfrogWidget, deviceScaleFactor)
animator = if (isTest) {
TestBackupAnimation(WalletBackupAnimator(cardsWidget), binding)
} else {
WalletBackupAnimator(cardsWidget)
}
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.onboardingWalletState == newState.onboardingWalletState
}.select { it.onboardingWalletState }
}
setStatusBarColor(R.color.background_primary)
}
override fun onStop() {
super.onStop()
onboardingManageTokensComponent = null
store.unsubscribe(this)
}
override fun newState(state: OnboardingWalletState) {
Handler(Looper.getMainLooper()).post {
if (activity == null || view == null) return@post
animator.updateBackupState(state.backupState)
requireActivity().invalidateOptionsMenu()
pbBinding.pbState.max = state.getMaxProgress()
pbBinding.pbState.progress = state.getProgressStep()
when {
state.wallet2State != null -> {
seedPhraseStateHandler.newState(this, state, seedPhraseViewModel)
seedPhraseViewModel.setCardArtworkUri(
cardArtworkUri = state.walletImages.primaryCardImage.toString(),
)
updateWalletImagesState(state.walletImages)
}
else -> {
updateWalletImagesState(state.walletImages)
handleOnboardingStep(state)
}
}
}
}
private fun updateWalletImagesState(walletImages: WalletImages) {
if (!walletImages.primaryCardImage.isNullOrEmpty()) {
loadImageIntoImageView(walletImages.primaryCardImage, binding.imvFrontCard)
}
if (!walletImages.secondCardImage.isNullOrEmpty()) {
loadImageIntoImageView(walletImages.secondCardImage, binding.imvFirstBackupCard)
}
if (!walletImages.thirdCardImage.isNullOrEmpty()) {
loadImageIntoImageView(walletImages.thirdCardImage, binding.imvSecondBackupCard)
}
}
private fun loadImageIntoImageView(uri: Uri?, view: ImageView) {
view.load(uri) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
}
internal fun handleOnboardingStep(state: OnboardingWalletState) {
when (state.step) {
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
OnboardingWalletStep.Backup -> setBackupState(state = state.backupState)
OnboardingWalletStep.ManageTokens -> setManageTokensState(requireNotNull(state.userWalletId))
OnboardingWalletStep.Done -> showSuccess()
else -> {}
}
}
private fun initializeOnboardingManageTokensComponent(userWalletId: UserWalletId) {
onboardingManageTokensComponent = onboardingManageTokensComponentFactory.create(
context = onboardingComponentContext,
params = OnboardingManageTokensComponent.Params(userWalletId = userWalletId),
onDone = {},
)
}
private fun setManageTokensState(userWalletId: UserWalletId) {
with(binding) {
tvHeader.show()
tvBody.show()
viewPagerBackupInfo.hide()
tabLayoutBackupInfo.hide()
onboardingWalletContainer.show()
layoutButtonsCommon.btnWalletAlternativeAction.hide()
onboardingWalletContainer.hide()
toolbar.title = getText(R.string.onboarding_add_tokens)
}
pbBinding.pbState.show()
bindingSeedPhrase.onboardingSeedPhraseContainer.hide()
if (onboardingManageTokensComponent == null) {
initializeOnboardingManageTokensComponent(userWalletId)
}
with(bindingManageTokens) {
onboardingManageTokensContainer.hide()
onboardingManageTokensContainer.show()
onboardingManageTokensContainer.setContent {
TangemTheme(
isDark = isSystemInDarkTheme(),
windowSize = rememberWindowSize(activity = requireActivity()),
) {
onboardingManageTokensComponent?.Content(modifier = Modifier.fillMaxSize())
}
}
}
}
private fun setupCreateWalletState() = with(binding) {
layoutButtonsCommon.btnWalletMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(OnboardingWalletAction.CreateWallet)
}
layoutButtonsCommon.btnWalletAlternativeAction.hide()
toolbar.title = getText(R.string.onboarding_getting_started)
tvHeader.setText(R.string.onboarding_create_wallet_header)
tvBody.setText(R.string.onboarding_create_wallet_body)
animator.setupCreateWalletState()
}
private fun setBackupState(state: BackupState) {
when (state.backupStep) {
null -> Unit
BackupStep.InitBackup -> showBackupIntro(state)
BackupStep.ScanOriginCard -> showScanOriginCard()
BackupStep.AddBackupCards -> showAddBackupCards(state)
BackupStep.SetAccessCode -> showSetAccessCode()
BackupStep.EnterAccessCode -> showEnterAccessCode(state)
BackupStep.ReenterAccessCode -> showReenterAccessCode(state)
is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state)
is BackupStep.WriteBackupCard -> showWriteBackupCard(state)
BackupStep.Finished -> {
// don't need to navigate here"
// showSuccess()
}
}
}
private fun showBackupIntro(state: BackupState) = with(binding) {
imvFirstBackupCard.show()
imvSecondBackupCard.show()
tvHeader.hide()
tvBody.hide()
viewPagerBackupInfo.show()
tabLayoutBackupInfo.show()
with(layoutButtonsCommon) {
btnWalletMainAction.text = getText(R.string.onboarding_button_backup_now)
btnWalletMainAction.icon = null
btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) }
btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
btnWalletAlternativeAction.setOnClickListener { store.dispatch(BackupAction.SkipBackup) }
btnWalletAlternativeAction.show(state.canSkipBackup && canSkipBackup)
}
animator.showBackupIntro(state)
}
private fun showScanOriginCard() = with(binding) {
prepareBackupView()
tvHeader.text = getText(R.string.onboarding_title_scan_origin_card)
tvBody.text = getString(
R.string.onboarding_subtitle_scan_primary,
)
with(layoutButtonsCommon) {
btnWalletMainAction.text = getString(R.string.onboarding_button_scan_origin_card)
btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
btnWalletAlternativeAction.hide()
btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) }
}
animator.showScanOriginCard()
}
private fun showAddBackupCards(state: BackupState) = with(binding) {
prepareBackupView()
accessCodeDialog?.dismiss()
accessCodeDialog = null
layoutButtonsAddCards.root.show()
layoutButtonsCommon.root.hide()
layoutButtonsAddCards.btnAddCard.setIconResource(R.drawable.ic_tangem_24)
if (state.showBtnLoading) {
layoutButtonsAddCards.btnAddCard.iconTint = ColorStateList.valueOf(
resources.getColor(R.color.button_secondary),
)
layoutButtonsAddCards.btnAddCard.text = ""
layoutButtonsAddCards.btnAddCard.isEnabled = false
layoutButtonsAddCards.btnProgress.show()
} else {
layoutButtonsAddCards.btnAddCard.iconTint = ColorStateList.valueOf(
resources.getColor(R.color.icon_primary_1),
)
layoutButtonsAddCards.btnProgress.hide()
layoutButtonsAddCards.btnAddCard.text = getText(R.string.onboarding_button_add_backup_card)
layoutButtonsAddCards.btnAddCard.isEnabled = true
}
if (state.backupCardsNumber < state.maxBackupCards) {
layoutButtonsAddCards.btnAddCard.setOnClickListener {
store.dispatch(BackupAction.AddBackupCard)
}
} else {
layoutButtonsAddCards.btnAddCard.isEnabled = false
}
layoutButtonsAddCards.btnContinue.text = getText(R.string.onboarding_button_finalize_backup)
layoutButtonsAddCards.btnContinue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) }
layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0
when (state.backupCardsNumber) {
0 -> {
tvHeader.text = getText(R.string.onboarding_title_no_backup_cards)
tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards)
}
1 -> {
tvHeader.text = getText(R.string.onboarding_title_one_backup_card)
tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card)
}
2 -> {
tvHeader.text = getText(R.string.onboarding_title_two_backup_cards)
tvBody.text = getText(R.string.onboarding_subtitle_two_backup_cards)
}
else -> {}
}
animator.showAddBackupCards(state, state.backupCardsNumber)
}
private fun prepareBackupView() = with(binding) {
toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup)
tvHeader.show()
tvBody.show()
viewPagerBackupInfo.hide()
tabLayoutBackupInfo.hide()
imvFirstBackupCard.show()
imvSecondBackupCard.show()
}
private fun showSetAccessCode() {
accessCodeDialog = AccessCodeDialog(requireContext()).apply {
dismissWithAnimation = true
create()
setOnCancelListener {
store.dispatch(BackupAction.OnAccessCodeDialogClosed)
}
setOnShowListener { setSecurityMode(true) }
setOnDismissListener { setSecurityMode(false) }
show()
showInfoScreen()
val view =
delegate.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
(view?.parent as? ViewGroup)?.let { TransitionManager.beginDelayedTransition(it) }
behavior.state = BottomSheetBehavior.STATE_EXPANDED
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
view?.let { it.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT }
}
}
private fun showEnterAccessCode(state: BackupState) {
accessCodeDialog?.showEnterAccessCode()
accessCodeDialog?.showError(state.accessCodeError)
}
private fun showReenterAccessCode(state: BackupState) {
accessCodeDialog?.showReenterAccessCode()
accessCodeDialog?.showError(state.accessCodeError)
}
private fun showWritePrimaryCard(state: BackupState) = with(binding) {
accessCodeDialog?.dismiss()
reInitCardsWidgetIfNeeded(state.backupCardsNumber)
prepareViewForFinalizeStep()
val isRing = state.primaryCardBatchId != null && isRing(batchId = state.primaryCardBatchId)
tvHeader.text = getText(
if (isRing) R.string.common_origin_ring else R.string.common_origin_card,
)
tvBody.text = if (isRing) {
getString(R.string.onboarding_subtitle_scan_ring)
} else {
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
getString(
R.string.onboarding_subtitle_scan_primary_card_format,
state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) },
)
}
layoutButtonsCommon.btnWalletMainAction.text = getText(
if (isRing) R.string.onboarding_button_backup_ring else R.string.onboarding_button_backup_origin,
)
layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) }
animator.showWritePrimaryCard(state)
}
private fun prepareViewForFinalizeStep() = with(binding) {
toolbar.title = getText(R.string.onboarding_button_finalize_backup)
layoutButtonsAddCards.root.hide()
layoutButtonsCommon.root.show()
layoutButtonsCommon.btnWalletAlternativeAction.hide()
imvCardBackground.hide()
imvFirstBackupCard.show()
imvSecondBackupCard.show()
}
private fun showWriteBackupCard(state: BackupState) = with(binding) {
reInitCardsWidgetIfNeeded(state.backupCardsNumber)
prepareViewForFinalizeStep()
val cardNumber = (state.backupStep as? BackupStep.WriteBackupCard)?.cardNumber ?: 1
val isRing = isRing(batchId = state.backupCardBatchIds[cardNumber - 1])
tvHeader.text = getString(
if (isRing) R.string.onboarding_title_backup_ring else R.string.onboarding_title_backup_card,
)
tvBody.text = if (isRing) {
getString(R.string.onboarding_subtitle_scan_ring)
} else {
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
getString(
R.string.onboarding_subtitle_scan_backup_card_format,
cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1]),
)
}
layoutButtonsCommon.btnWalletMainAction.text = getString(
if (isRing) R.string.onboarding_button_backup_ring else R.string.onboarding_button_backup_origin,
)
layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
store.dispatch(BackupAction.WriteBackupCard(cardNumber))
}
animator.showWriteBackupCard(state, cardNumber)
}
private fun showSuccess() = with(binding) {
toolbar.title = getString(R.string.onboarding_done_header)
tvHeader.text = getText(R.string.onboarding_done_header)
tvHeader.show()
tvBody.show()
viewPagerBackupInfo.hide()
tabLayoutBackupInfo.hide()
onboardingWalletContainer.show()
bindingManageTokens.onboardingManageTokensContainer.hide()
tvBody.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding)
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_continue_wallet)
layoutButtonsCommon.btnWalletMainAction.icon = null
layoutButtonsCommon.btnWalletAlternativeAction.hide()
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingWalletAction.FinishOnboarding(scope = requireActivity().lifecycleScope))
}
animator.showSuccess {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
flCardsContainer.hide()
imvCardBackground.hide()
showConfetti(true)
imvSuccess.alpha = 0f
imvSuccess.show()
imvSuccess.animate()?.alpha(1f)?.duration = 400
}
}
}
internal fun showConfetti(show: Boolean) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
with(binding.vConfetti) {
lavConfetti.show(show)
if (show) {
lavConfetti.playAnimation()
} else {
lavConfetti.cancelAnimation()
}
}
}
}
override fun handleOnBackPressed() {
// workaround to use right navigation back for toolbar back btn on seed phrase flow
val isWallet2 =
store.state.globalState.onboardingState.onboardingManager?.scanResponse?.cardTypesResolver?.isWallet2()
?: false
val seedPhraseRouter = seedPhraseRouter
if (seedPhraseRouter != null && isWallet2) {
seedPhraseRouter.navigateBack()
} else {
legacyOnBackHandler()
}
}
private fun legacyOnBackHandler() {
store.dispatch(OnboardingWalletAction.OnBackPressed)
}
private fun getDeviceScaleFactor(): Float {
val typedValue = TypedValue()
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
return typedValue.float
}
private fun makeSeedPhraseRouter(): SeedPhraseRouter = SeedPhraseRouter(
onBack = ::legacyOnBackHandler,
onOpenChat = {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
val scanResponse = requireNotNull(store.state.globalState.onboardingState.onboardingManager?.scanResponse)
val getCardInfoUseCase = store.inject(DaggerGraphState::getCardInfoUseCase)
val cardInfo = requireNotNull(getCardInfoUseCase.invoke(scanResponse).getOrNull())
scope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.DirectUserRequest(cardInfo))
}
},
onOpenUriClick = { uri ->
store.dispatchOpenUrl(uri.toString())
},
)
private fun makeSeedPhraseMediator(): SeedPhraseMediator {
return object : SeedPhraseMediator {
override fun createWallet(callback: (CompletionResult<CreateWalletResponse>) -> Unit) {
store.dispatch(OnboardingWallet2Action.CreateWallet(callback))
}
override fun onWalletCreated(result: CompletionResult<CreateWalletResponse>) {
store.dispatch(OnboardingWallet2Action.WalletWasCreated(result))
}
override fun importWallet(
mnemonicComponents: List<String>,
passphrase: String?,
seedPhraseSource: SeedPhraseSource,
callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) {
store.dispatch(
OnboardingWallet2Action.ImportWallet(
mnemonicComponents,
passphrase,
seedPhraseSource,
callback,
),
)
}
override fun allowScreenshots(allow: Boolean) {
setSecurityMode(value = !allow)
}
}
}
private fun setSecurityMode(value: Boolean) {
Timber.d("Security mode: ${if (value) "enabled" else "disabled"}")
if (value) {
requireActivity().window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
} else {
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}

View file

@ -1,240 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.view.View
import android.widget.ImageView
import androidx.core.animation.doOnEnd
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapView
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
class WalletCardsWidget(
val leapfrogWidget: LeapfrogWidget,
private val deviceScaleFactor: Float = 1f,
) {
private val animDuration: Long = 400
private var currentState: WidgetState? = null
fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) {
if (currentState == WidgetState.WELCOME) return
currentState = WidgetState.WELCOME
val animator = createAnimator(animate, onEnd = onEnd)
animator.playTogether(createAnimators(::createWelcomeProperties))
leapfrogWidget.fold { animator.start() }
}
fun toFolded(animate: Boolean = true, duration: Long = animDuration, onEnd: () -> Unit = {}) {
if (currentState == WidgetState.FOLDED) return
currentState = WidgetState.FOLDED
val animator = createAnimator(animate, duration) {
leapfrogWidget.fold(animate, onEnd)
}
animator.playTogether(createAnimators(::createLeapfrogProperties))
animator.start()
}
fun toFan(animate: Boolean = true, onEnd: () -> Unit = {}) {
if (currentState == WidgetState.FAN) return
currentState = WidgetState.FAN
val animator = createAnimator(animate, onEnd = onEnd)
animator.playTogether(createAnimators(::createFanProperties))
leapfrogWidget.fold { animator.start() }
}
fun toLeapfrog(animate: Boolean = true, onEndFold: () -> Unit = {}, onEnd: () -> Unit = {}) {
if (currentState == WidgetState.LEAPFROG) return
currentState = WidgetState.LEAPFROG
val onAnimatorEnd = {
leapfrogWidget.fold(animate) {
leapfrogWidget.initViews()
onEndFold()
leapfrogWidget.unfold(animate, onEnd)
}
}
val animator = createAnimator(animate, onEnd = onAnimatorEnd)
animator.playTogether(createAnimators(::createLeapfrogProperties))
animator.start()
}
private fun createAnimators(propertyFactory: (BackupCardType) -> CardProperties): List<ObjectAnimator> {
return getCardTypesTypes().map { createCardAnimator(it, propertyFactory(it)) }
}
@Suppress("MagicNumber")
private fun getCardTypesTypes(): List<BackupCardType> = when (leapfrogWidget.getViewsCount()) {
2 -> listOf(BackupCardType.ORIGIN, BackupCardType.FIRST_BACKUP)
3 -> listOf(BackupCardType.ORIGIN, BackupCardType.FIRST_BACKUP, BackupCardType.SECOND_BACKUP)
else -> throw UnsupportedOperationException()
}
private fun createAnimator(animate: Boolean, duration: Long = animDuration, onEnd: () -> Unit): AnimatorSet {
return AnimatorSet().apply {
this.duration = if (animate) duration else 0
doOnEnd { onEnd() }
}
}
private fun createCardAnimator(cardType: BackupCardType, properties: CardProperties): ObjectAnimator {
val view = getLeapViewByCard(cardType).view
val animator = ObjectAnimator.ofPropertyValuesHolder(
view,
*properties.createValuesHolders().toTypedArray(),
)
view.elevation = properties.elevation
return animator
}
@Suppress("MagicNumber")
private fun createWelcomeProperties(cardType: BackupCardType): CardProperties {
return when (cardType) {
BackupCardType.ORIGIN -> CardProperties(
xTranslation = 440f * deviceScaleFactor,
yTranslation = 70f,
rotation = 75f,
elevation = 2f,
scale = .85f * deviceScaleFactor,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 10f * deviceScaleFactor,
yTranslation = -30f,
rotation = 100f,
elevation = 1f,
scale = .85f * deviceScaleFactor,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = -440f * deviceScaleFactor,
yTranslation = -30f,
rotation = 70f,
elevation = 0f,
scale = .85f * deviceScaleFactor,
)
}
}
@Suppress("MagicNumber")
private fun createFanProperties(cardType: BackupCardType): CardProperties {
return when (cardType) {
BackupCardType.ORIGIN -> CardProperties(
xTranslation = 0f,
yTranslation = 30f,
rotation = 5f,
elevation = 2f,
scale = 1f * deviceScaleFactor,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = -50f,
rotation = -5f,
elevation = 1f,
scale = 0.9f * deviceScaleFactor,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = 10f,
yTranslation = -120f,
rotation = -20f,
elevation = 0f,
scale = 0.8f * deviceScaleFactor,
)
}
}
@Suppress("MagicNumber")
private fun createLeapfrogProperties(cardType: BackupCardType): CardProperties {
return when (cardType) {
BackupCardType.ORIGIN -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
elevation = 2f,
scale = 1f * deviceScaleFactor,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
elevation = 1f,
scale = 0.9f * deviceScaleFactor,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
elevation = 0f,
scale = 0.8f * deviceScaleFactor,
)
}
}
@Throws(UnsupportedOperationException::class)
fun getOriginCardView(): ImageView {
return getLeapViewByCard(BackupCardType.ORIGIN).view as ImageView
}
@Throws(UnsupportedOperationException::class)
fun getFirstBackupCardView(): ImageView {
return getLeapViewByCard(BackupCardType.FIRST_BACKUP).view as ImageView
}
@Throws(UnsupportedOperationException::class)
fun getSecondBackupCardView(): ImageView {
return getLeapViewByCard(BackupCardType.SECOND_BACKUP).view as ImageView
}
@Suppress("MagicNumber")
@Throws(UnsupportedOperationException::class)
private fun getLeapViewByCard(cardType: BackupCardType): LeapView {
fun getByIndex(index: Int): LeapView {
return leapfrogWidget.getViewByPosition(leapfrogWidget.getViewPositionByIndex(index))
}
return when (leapfrogWidget.getViewsCount()) {
2 -> when (cardType) {
BackupCardType.ORIGIN -> getByIndex(1)
BackupCardType.FIRST_BACKUP -> getByIndex(0)
BackupCardType.SECOND_BACKUP -> throw UnsupportedOperationException()
}
3 -> when (cardType) {
BackupCardType.ORIGIN -> getByIndex(2)
BackupCardType.FIRST_BACKUP -> getByIndex(1)
BackupCardType.SECOND_BACKUP -> getByIndex(0)
}
else -> throw UnsupportedOperationException()
}
}
enum class WidgetState { WELCOME, FOLDED, FAN, LEAPFROG }
}
enum class BackupCardType { ORIGIN, FIRST_BACKUP, SECOND_BACKUP }
private data class CardProperties(
val xTranslation: Float = 0f,
val yTranslation: Float = 0f,
val rotation: Float = 0f,
val elevation: Float = 0f,
val scale: Float = 1.1f,
) {
fun createValuesHolders(): List<PropertyValuesHolder> {
return listOf(
PropertyValuesHolder.ofFloat(View.TRANSLATION_X, xTranslation),
PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, yTranslation),
PropertyValuesHolder.ofFloat(View.ROTATION, rotation),
PropertyValuesHolder.ofFloat(View.SCALE_X, scale),
PropertyValuesHolder.ofFloat(View.SCALE_Y, scale),
)
}
companion object
}

View file

@ -1,81 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.onboarding.products.wallet.redux.AccessCodeError
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutBackupAccessCodeBinding
class AccessCodeDialog(context: Context) : BottomSheetDialog(context) {
private var binding: LayoutBackupAccessCodeBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutBackupAccessCodeBinding
.inflate(LayoutInflater.from(context))
setContentView(binding!!.root)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
override fun setOnDismissListener(listener: DialogInterface.OnDismissListener?) {
super.setOnDismissListener {
listener?.onDismiss(it)
binding = null
}
}
fun showInfoScreen() = binding?.run {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
accessCodeTitle.text = context.getText(R.string.onboarding_access_code_intro_title)
layoutBackupAccessCodeInfo.root.show()
layoutBackupAccessCodeSubmit.root.hide()
layoutBackupAccessCodeInfo.btnAccessCodeCreate.setOnClickListener {
store.dispatch(BackupAction.ShowEnterAccessCodeScreen)
}
}
fun showEnterAccessCode() = binding?.run {
layoutBackupAccessCodeInfo.root.hide()
layoutBackupAccessCodeSubmit.root.show()
accessCodeTitle.text = context.getText(R.string.onboarding_access_code_intro_title)
with(layoutBackupAccessCodeSubmit) {
btnAccessCodeSubmit.text = context.getText(R.string.common_continue)
btnAccessCodeSubmit.setOnClickListener {
store.dispatch(BackupAction.CheckAccessCode(etAccessCode.text.toString()))
}
}
}
fun showReenterAccessCode() = binding?.run {
layoutBackupAccessCodeInfo.root.hide()
layoutBackupAccessCodeSubmit.root.show()
accessCodeTitle.text = context.getText(R.string.onboarding_access_code_repeat_code_title)
with(layoutBackupAccessCodeSubmit) {
etAccessCode.setText("")
btnAccessCodeSubmit.text = context.getText(R.string.common_submit)
btnAccessCodeSubmit.setOnClickListener {
store.dispatch(BackupAction.SaveAccessCodeConfirmation(etAccessCode.text.toString()))
}
}
}
fun showError(error: AccessCodeError?) = binding?.layoutBackupAccessCodeSubmit?.run {
when (error) {
AccessCodeError.CodeTooShort ->
tilAccessCode.error = context.getString(R.string.onboarding_access_code_too_short)
AccessCodeError.CodesDoNotMatch ->
tilAccessCode.error = context.getString(R.string.onboarding_access_codes_doesnt_match)
null -> tilAccessCode.error = null
}
}
}

View file

@ -1,26 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
object AddMoreBackupCardsDialog {
fun create(context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.common_warning)
setMessage(R.string.onboarding_alert_message_not_max_backup_cards_added)
setPositiveButton(R.string.common_continue) { _, _ ->
store.dispatch(BackupAction.ShowAccessCodeInfoScreen)
}
setNegativeButton(R.string.common_cancel) { _, _ ->
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,24 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
internal object AttestationFailedDialog {
fun create(context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.common_error)
setMessage(R.string.issuer_signature_loading_failed)
setPositiveButton(R.string.common_ok) { dialog, _ ->
dialog.dismiss()
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,23 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
object BackupInProgressDialog {
fun create(context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.common_warning)
setMessage(R.string.onboarding_backup_exit_warning)
setPositiveButton(R.string.warning_button_ok) { _, _ ->
store.dispatch(GlobalAction.HideDialog)
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,25 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.app.Dialog
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object InterruptOnboardingDialog {
fun create(context: Context, dialog: OnboardingDialog.InterruptOnboarding): Dialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.onboarding_exit_alert_title))
setMessage(context.getString(R.string.onboarding_exit_alert_message))
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onOk() }
setNegativeButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatchDialogHide() }
setCancelable(false)
}.create()
}
}

View file

@ -1,30 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
object ResetBackupCardDialog {
fun create(context: Context, cardId: String): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.common_attention)
setMessage(R.string.onboarding_linking_error_card_with_wallets)
setPositiveButton(R.string.common_cancel) { _, _ ->
Analytics.send(Onboarding.Backup.ResetCancelEvent)
}
setNegativeButton(R.string.card_settings_action_sheet_reset) { _, _ ->
Analytics.send(Onboarding.Backup.ResetPerformEvent)
store.dispatch(BackupAction.ResetBackupCard(cardId))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -33,10 +33,6 @@ import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import com.tangem.tap.features.home.api.HomeComponent
import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment
import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragment
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.routing.component.RoutingComponent.Child
@ -423,11 +419,7 @@ internal class ChildFactory @Inject constructor(
componentFactory = nftSendComponentFactory,
)
}
is AppRoute.OnboardingNote,
is AppRoute.SaveWallet,
is AppRoute.OnboardingOther,
is AppRoute.OnboardingTwins,
is AppRoute.OnboardingWallet,
-> error("Unsupported route: $route")
}
// endregion
@ -545,18 +537,6 @@ internal class ChildFactory @Inject constructor(
componentFactory = manageTokensComponentFactory,
)
}
is AppRoute.OnboardingNote -> {
route.asFragmentChild(Provider { OnboardingNoteFragment() })
}
is AppRoute.OnboardingOther -> {
route.asFragmentChild(Provider { OnboardingOtherCardsFragment() })
}
is AppRoute.OnboardingTwins -> {
route.asFragmentChild(Provider { OnboardingTwinsFragment() })
}
is AppRoute.OnboardingWallet -> {
route.asFragmentChild(Provider { OnboardingWalletFragment() })
}
is AppRoute.QrScanning -> {
val source = when (route.source) {
is AppRoute.QrScanning.Source.Send -> SourceType.SEND

View file

@ -40,27 +40,6 @@ sealed class AppRoute(val path: String) : Route {
val isTosAccepted: Boolean,
) : AppRoute(path = "/disclaimer${if (isTosAccepted) "/tos_accepted" else ""}")
@Serializable
data object OnboardingNote : AppRoute(path = "/onboarding/note")
@Serializable
data class OnboardingWallet(
val canSkipBackup: Boolean = true,
) : AppRoute(path = "/onboarding/wallet${if (canSkipBackup) "/skippable" else ""}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val CAN_SKIP_BACKUP_KEY = "canSkipBackup"
}
}
@Serializable
data object OnboardingTwins : AppRoute(path = "/onboarding/twins")
@Serializable
data object OnboardingOther : AppRoute(path = "/onboarding/other")
@Serializable
data object Wallet : AppRoute(path = "/wallet")

View file

@ -7,10 +7,6 @@
"name": "ONRAMP_ENABLED",
"version": "undefined"
},
{
"name": "ONBOARDING_CODE_REFACTORING_ENABLED",
"version": "5.22.0"
},
{
"name": "NAVIGATION_REFACTORING",
"version": "5.23.0"
@ -47,10 +43,6 @@
"name": "ASK_BIOMETRY_REFACTORING_ENABLED",
"version": "5.22.0"
},
{
"name": "NOTE_REFACTORING_ENABLED",
"version": "5.23.0"
},
{
"name": "NEW_ARTWORK_LOADING",
"version": "5.24.0"
@ -59,10 +51,6 @@
"name": "NEW_ATTESTATION_ENABLED",
"version": "undefined"
},
{
"name": "TWIN_REFACTORING_ENABLED",
"version": "5.23.0"
},
{
"name": "SEND_V2_ENABLED",
"version": "5.23.0"

View file

@ -1,16 +1,8 @@
package com.tangem.domain.redux
import com.tangem.domain.models.scan.ScanResponse
import org.rekotlin.Action
sealed interface LegacyAction : Action {
/**
* Initiate an onboarding process.
* For resuming unfinished backup of standard Wallet see
* BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup
*/
data class StartOnboardingProcess(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : LegacyAction
data object PrepareDetailsScreen : LegacyAction
}

View file

@ -1,8 +1,5 @@
package com.tangem.features.onboarding.v2
interface OnboardingV2FeatureToggles {
val isOnboardingV2Enabled: Boolean
val isVisaOnboardingEnabled: Boolean
val isNoteRefactoringEnabled: Boolean
val isTwinRefactoringEnabled: Boolean
}

View file

@ -6,12 +6,6 @@ import javax.inject.Inject
internal class DefaultOnboardingV2FeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) : OnboardingV2FeatureToggles {
override val isOnboardingV2Enabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("ONBOARDING_CODE_REFACTORING_ENABLED")
override val isVisaOnboardingEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("VISA_ONBOARDING_ENABLED")
override val isNoteRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("NOTE_REFACTORING_ENABLED")
override val isTwinRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("TWIN_REFACTORING_ENABLED")
}

View file

@ -1 +0,0 @@
/build

View file

@ -1,64 +0,0 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.feature.onboarding"
}
dependencies {
/** Core modules */
implementation(project(":common"))
implementation(project(":domain:models"))
implementation(project(":core:config-toggles"))
implementation(project(":core:datasource"))
implementation(project(":core:analytics"))
implementation(projects.core.analytics.models)
implementation(project(":core:utils"))
implementation(project(":core:ui"))
implementation(project(":core:res"))
implementation(project(":data:common"))
/** Tangem libraries */
implementation(tangemDeps.card.core)
implementation(tangemDeps.card.android) {
exclude(module = "joda-time")
}
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
implementation(deps.androidx.appCompat)
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.constraintLayout)
implementation(deps.androidx.activity.compose)
implementation(deps.lifecycle.runtime.ktx)
implementation(deps.lifecycle.common.java8)
implementation(deps.lifecycle.viewModel.ktx)
/** Compose libraries */
implementation(deps.compose.material)
implementation(deps.compose.material3)
implementation(deps.compose.animation)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.coil)
implementation(deps.compose.shimmer)
implementation(tangemDeps.card.core)
/** Other libraries */
implementation(deps.compose.shimmer)
implementation(deps.kotlin.immutable.collections)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -1,14 +0,0 @@
package com.tangem.feature.onboarding.api
import androidx.compose.runtime.Composable
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen
/**
[REDACTED_AUTHOR]
*/
interface OnboardingSeedPhraseApi {
@Suppress("TopLevelComposableFunctions")
@Composable
fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float)
}

View file

@ -1,71 +0,0 @@
package com.tangem.feature.onboarding.api
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.ui.*
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen
/**
[REDACTED_AUTHOR]
*/
class OnboardingSeedPhraseScreen : OnboardingSeedPhraseApi {
@Composable
override fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float) {
BackHandler(onBack = uiState.onBackClick)
Column {
TangemLinearProgressIndicator(
progress = { progress },
color = TangemTheme.colors.icon.primary1,
backgroundColor = TangemTheme.colors.icon.primary1.copy(alpha = 0.4f),
strokeCap = StrokeCap.Round,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = TangemTheme.dimens.size8)
.padding(horizontal = TangemTheme.dimens.size16),
)
Content(subScreen, uiState)
}
}
}
@Composable
private fun Content(screen: SeedPhraseScreen, uiState: OnboardingSeedPhraseState) {
when (screen) {
SeedPhraseScreen.Intro -> {
IntroScreen(
state = uiState.introState,
)
}
SeedPhraseScreen.AboutSeedPhrase -> {
AboutSeedPhraseScreen(
state = uiState.aboutState,
)
}
SeedPhraseScreen.YourSeedPhrase -> {
YourSeedPhraseScreen(
state = uiState.yourSeedPhraseState,
)
}
SeedPhraseScreen.CheckSeedPhrase -> {
CheckSeedPhraseScreen(
state = uiState.checkSeedPhraseState,
)
}
SeedPhraseScreen.ImportSeedPhrase -> {
ImportSeedPhraseScreen(
state = uiState.importSeedPhraseState,
)
}
}
}

View file

@ -1,32 +0,0 @@
package com.tangem.feature.onboarding.data
import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.bip39.EntropyLength
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.Wordlist
import com.tangem.feature.onboarding.domain.models.MnemonicType
/**
[REDACTED_AUTHOR]
*/
class DefaultMnemonicRepository(
private val bip39Wordlist: Wordlist,
) : MnemonicRepository {
private val bip39Words: Set<String> by lazy { bip39Wordlist.words.toHashSet() }
override fun getWordsDictionary(): Set<String> = bip39Words
override fun generateDefaultMnemonic(): Mnemonic {
return DefaultMnemonic(EntropyLength.Bits128Length, bip39Wordlist)
}
override fun generateMnemonic(mnemonicType: MnemonicType): Mnemonic {
return when (mnemonicType) {
MnemonicType.Mnemonic12 -> DefaultMnemonic(EntropyLength.Bits128Length, bip39Wordlist)
MnemonicType.Mnemonic24 -> DefaultMnemonic(EntropyLength.Bits256Length, bip39Wordlist)
}
}
override fun createMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(mnemonicString, bip39Wordlist)
}

View file

@ -1,22 +0,0 @@
package com.tangem.feature.onboarding.data
import com.tangem.common.core.TangemSdkError
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.feature.onboarding.domain.models.MnemonicType
/**
[REDACTED_AUTHOR]
*/
interface MnemonicRepository {
fun getWordsDictionary(): Set<String>
@Throws(TangemSdkError.MnemonicException::class)
fun generateDefaultMnemonic(): Mnemonic
@Throws(TangemSdkError.MnemonicException::class)
fun generateMnemonic(mnemonicType: MnemonicType): Mnemonic
@Throws(TangemSdkError.MnemonicException::class)
fun createMnemonic(mnemonicString: String): Mnemonic
}

View file

@ -1,15 +0,0 @@
package com.tangem.feature.onboarding.data.model
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.derivation.ExtendedPublicKeysMap
/**
[REDACTED_AUTHOR]
*/
data class CreateWalletResponse(
val card: CardDTO,
val derivedKeys: Map<ByteArrayKey, ExtendedPublicKeysMap> = mapOf(),
val primaryCard: PrimaryCard? = null,
)

View file

@ -1,20 +0,0 @@
package com.tangem.feature.onboarding.di
import com.tangem.feature.onboarding.navigation.DefaultOnboardingRouter
import com.tangem.feature.onboarding.navigation.OnboardingRouter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
@Module
@InstallIn(ActivityComponent::class)
internal object OnboardingRouterModule {
@Provides
@ActivityScoped
fun provideOnboardingRouter(): OnboardingRouter {
return DefaultOnboardingRouter()
}
}

View file

@ -1,113 +0,0 @@
package com.tangem.feature.onboarding.domain
import com.tangem.common.core.TangemSdkError
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.MnemonicErrorResult
import com.tangem.feature.onboarding.data.MnemonicRepository
import com.tangem.feature.onboarding.domain.models.MnemonicType
import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField
import com.tangem.utils.extensions.isNotWhitespace
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
/**
[REDACTED_AUTHOR]
*/
internal class DefaultSeedPhraseInteractor(
private val repository: MnemonicRepository,
) : SeedPhraseInteractor {
private val partWordFinder: PartWordFinder = PartWordFinder()
override suspend fun generateMnemonic(): Result<Mnemonic> {
return runCatching {
repository.generateDefaultMnemonic()
}
}
override suspend fun generateMnemonics(mnemonicTypes: List<MnemonicType>): Result<Map<MnemonicType, Mnemonic>> {
return runCatching {
mnemonicTypes.associateWith {
repository.generateMnemonic(it)
}
}
}
override suspend fun isWordMatch(mnemonicComponents: List<String>?, field: SeedPhraseField, word: String): Boolean {
return mnemonicComponents?.get(field.index) == word
}
override suspend fun validateMnemonicString(text: String): Result<List<String>> {
val inputWords = text.split("\\s+".toRegex()).toSet()
val wordsDictionary = repository.getWordsDictionary()
val invalidWords = inputWords
.filter { it.isNotEmpty() && !wordsDictionary.contains(it) }
.toSet()
return if (invalidWords.isNotEmpty()) {
Result.failure(SeedPhraseError.InvalidWords(invalidWords))
} else {
try {
val mnemonic = repository.createMnemonic(text)
Result.success(mnemonic.mnemonicComponents)
} catch (ex: TangemSdkError.MnemonicException) {
val error = ex.mnemonicResult.mapToError()
Result.failure(error)
}
}
}
override suspend fun getSuggestions(
text: String,
hasSelection: Boolean,
cursorPosition: Int,
): ImmutableList<String> {
if (text.isEmpty() || cursorPosition == 0 || hasSelection) return persistentListOf()
val word = partWordFinder.getLeadPartOfWord(text, cursorPosition)
?: return persistentListOf()
val suggestions = repository.getWordsDictionary()
.filter { it.startsWith(word, ignoreCase = false) && it != word }
.toPersistentList()
return suggestions
}
override suspend fun insertSuggestionWord(
text: String,
suggestion: String,
cursorPosition: Int,
): InsertSuggestionResult {
val textPartLead = text.substring(0, cursorPosition)
val textPartLast = text.substring(cursorPosition, text.length)
val wordPartLast = partWordFinder.getLastPartOfWord(text, suggestion, cursorPosition)
val textBuilder = StringBuilder()
.append(textPartLead)
.append(wordPartLast)
if (textPartLast.isEmpty() || textPartLast[0].isNotWhitespace()) {
textBuilder.append(SeedPhraseInteractor.MNEMONIC_DELIMITER)
}
textBuilder.append(textPartLast)
val textWithSuggestion = textBuilder.toString()
val newCursorPosition = cursorPosition + wordPartLast.length + SeedPhraseInteractor.MNEMONIC_DELIMITER.length
return InsertSuggestionResult(textWithSuggestion, newCursorPosition)
}
}
private fun MnemonicErrorResult.mapToError(): SeedPhraseError = when (this) {
MnemonicErrorResult.InvalidWordCount -> SeedPhraseError.InvalidWordCount
MnemonicErrorResult.InvalidEntropyLength -> SeedPhraseError.InvalidEntropyLength
MnemonicErrorResult.InvalidWordsFile -> SeedPhraseError.InvalidWordsFile
MnemonicErrorResult.InvalidChecksum -> SeedPhraseError.InvalidChecksum
MnemonicErrorResult.MnenmonicCreationFailed -> SeedPhraseError.MnenmonicCreationFailed
MnemonicErrorResult.NormalizationFailed -> SeedPhraseError.NormalizationFailed
MnemonicErrorResult.UnsupportedLanguage -> SeedPhraseError.UnsupportedLanguage
is MnemonicErrorResult.InvalidWords -> SeedPhraseError.InvalidWords(this.words)
MnemonicErrorResult.InvalidMnemonic -> SeedPhraseError.InvalidMnemonic
}

View file

@ -1,41 +0,0 @@
package com.tangem.feature.onboarding.domain
import com.tangem.common.module.ModuleError
import com.tangem.common.module.ModuleErrorCode
import com.tangem.common.module.ModuleMessage
/**
[REDACTED_AUTHOR]
* For each pool of errors, use the codes lying in range 100 (200..299, 300..399, etc)
*/
abstract class OnboardingModuleError(
subCode: Int,
override val message: String,
override val data: Any?,
) : ModuleMessage, ModuleError() {
override val code: Int = ModuleErrorCode.ONBOARDING + subCode
companion object {
internal const val ERROR_CODE_SEED_PHRASE = 100
}
}
sealed class SeedPhraseError(
subCode: Int = 0,
message: String? = null,
data: Any? = null,
) : OnboardingModuleError(
subCode = ERROR_CODE_SEED_PHRASE + subCode,
message = message ?: this::class.java.simpleName,
data = data,
) {
object InvalidWordCount : SeedPhraseError(subCode = 0)
object InvalidEntropyLength : SeedPhraseError(subCode = 1)
object InvalidWordsFile : SeedPhraseError(subCode = 2)
object InvalidChecksum : SeedPhraseError(subCode = 3)
object MnenmonicCreationFailed : SeedPhraseError(subCode = 4)
object NormalizationFailed : SeedPhraseError(subCode = 5)
object UnsupportedLanguage : SeedPhraseError(subCode = 6)
data class InvalidWords(val words: Set<String>) : SeedPhraseError(subCode = 7)
object InvalidMnemonic : SeedPhraseError(subCode = 8)
}

View file

@ -1,50 +0,0 @@
package com.tangem.feature.onboarding.domain
import com.tangem.utils.extensions.isNotWhitespace
/**
[REDACTED_AUTHOR]
* Class which provides helper functions to determine lead or last part of words for suggestions
*/
internal class PartWordFinder {
/**
* Searches and return part of a word on the left side of the [cursorPosition] into the [text]
*/
fun getLeadPartOfWord(text: String, cursorPosition: Int): String? {
val charText = text.toCharArray().asList()
val cursorIsOutOfTextRange = cursorPosition > charText.size || cursorPosition < 0
val cursorAtStartOfText = cursorPosition == 0
val hadWhiteSpaceOnLeftOfCursor = charText[cursorPosition - 1].isWhitespace()
val cursorInMiddleOfWord = charText.size > cursorPosition && charText[cursorPosition].isNotWhitespace()
@Suppress("ComplexCondition")
if (cursorIsOutOfTextRange || cursorAtStartOfText || hadWhiteSpaceOnLeftOfCursor || cursorInMiddleOfWord) {
return null
}
for (charIndex in cursorPosition - 1 downTo 0) {
val char = charText[charIndex]
when {
charIndex == 0 -> {
return text.substring(charIndex, cursorPosition)
}
char.isWhitespace() -> {
return text.substring(charIndex + 1, cursorPosition)
}
}
}
return null
}
/**
* Returns a substring of the [word] which excludes the first word part found on the left of [cursorPosition] in
* the [text]
*/
fun getLastPartOfWord(text: String, word: String, cursorPosition: Int): String {
val leadPart = getLeadPartOfWord(text, cursorPosition) ?: return ""
return word.substring(leadPart.length, word.length)
}
}

View file

@ -1,27 +0,0 @@
package com.tangem.feature.onboarding.domain
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.feature.onboarding.domain.models.MnemonicType
import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField
import kotlinx.collections.immutable.ImmutableList
/**
[REDACTED_AUTHOR]
*/
interface SeedPhraseInteractor {
suspend fun generateMnemonic(): Result<Mnemonic>
suspend fun generateMnemonics(mnemonicTypes: List<MnemonicType>): Result<Map<MnemonicType, Mnemonic>>
suspend fun isWordMatch(mnemonicComponents: List<String>?, field: SeedPhraseField, word: String): Boolean
suspend fun validateMnemonicString(text: String): Result<List<String>>
suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): ImmutableList<String>
suspend fun insertSuggestionWord(text: String, suggestion: String, cursorPosition: Int): InsertSuggestionResult
companion object {
const val MNEMONIC_DELIMITER = " "
}
}
data class InsertSuggestionResult(
val text: String,
val cursorPosition: Int,
)

View file

@ -1,21 +0,0 @@
package com.tangem.feature.onboarding.domain.di
import com.tangem.feature.onboarding.data.MnemonicRepository
import com.tangem.feature.onboarding.domain.DefaultSeedPhraseInteractor
import com.tangem.feature.onboarding.domain.SeedPhraseInteractor
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class OnboardingDomainModule {
@Provides
@Singleton
fun provideSeedPhraseInteractor(repository: MnemonicRepository): SeedPhraseInteractor {
return DefaultSeedPhraseInteractor(repository)
}
}

View file

@ -1,5 +0,0 @@
package com.tangem.feature.onboarding.domain.models
enum class MnemonicType {
Mnemonic12, Mnemonic24;
}

View file

@ -1,3 +0,0 @@
package com.tangem.feature.onboarding.navigation
class DefaultOnboardingRouter : OnboardingRouter

View file

@ -1,7 +0,0 @@
package com.tangem.feature.onboarding.navigation
/**
* Onboarding router
*/
// TODO: Move to onboarding api module [REDACTED_JIRA]
interface OnboardingRouter

View file

@ -1,41 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class SeedPhraseEvents(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(ONBOARDING_SEED_PHRASE, event, params) {
object ButtonGenerateSeedPhrase : SeedPhraseEvents("Button - Generate Seed Phrase")
object ButtonImportWallet : SeedPhraseEvents("Button - Import Wallet")
object ButtonReadMore : SeedPhraseEvents("Button - Read More")
object ButtonImport : SeedPhraseEvents("Button - Import")
object IntroScreenOpened : SeedPhraseEvents("Seed Intro Screen Opened")
object GenerationScreenOpened : SeedPhraseEvents("Seed Generation Screen Opened")
object CheckingScreenOpened : SeedPhraseEvents("Seed Checking Screen Opened")
object ImportScreenOpened : SeedPhraseEvents("Import Seed Phrase Screen Opened")
companion object {
const val ONBOARDING_SEED_PHRASE = "Onboarding / Seed Phrase"
}
}
sealed class CreateWalletEvents(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(ONBOARDING_CREATE_WALLET, event, params) {
object OnboardingSeedButtonCreateWallet : CreateWalletEvents("Button - Create Wallet")
object OnboardingSeedButtonOtherOptions : CreateWalletEvents("Button - Other Options")
companion object {
const val ONBOARDING_CREATE_WALLET = "Onboarding / Create Wallet"
}
}
enum class SeedPhraseSource {
IMPORTED, GENERATED
}

View file

@ -1,11 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.model
/**
[REDACTED_AUTHOR]
*/
data class ButtonState(
val enabled: Boolean = true,
val isClickable: Boolean = true,
val showProgress: Boolean = false,
val onClick: () -> Unit,
)

View file

@ -1,8 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.model
import androidx.annotation.StringRes
data class DescriptionResource(
@StringRes val titleRes: Int,
@StringRes val subTitleRes: Int,
)

View file

@ -1,102 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.model
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.feature.onboarding.domain.SeedPhraseError
import com.tangem.feature.onboarding.domain.models.MnemonicType
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
/**
[REDACTED_AUTHOR]
*/
data class OnboardingSeedPhraseState(
val introState: IntroState,
val aboutState: AboutState,
val yourSeedPhraseState: YourSeedPhraseState,
val checkSeedPhraseState: CheckSeedPhraseState,
val importSeedPhraseState: ImportSeedPhraseState,
val menuButtonChat: ButtonState,
val onBackClick: () -> Unit,
)
data class IntroState(
val cardImageUrl: String? = null,
val buttonCreateWallet: ButtonState,
val buttonOtherOptions: ButtonState,
)
data class AboutState(
val buttonReadMoreAboutSeedPhrase: ButtonState,
val buttonGenerateSeedPhrase: ButtonState,
val buttonImportSeedPhrase: ButtonState,
)
data class YourSeedPhraseState(
val segmentSeedState: SegmentSeedState,
val mnemonicGridItems: PersistentList<MnemonicGridItem> = persistentListOf(),
val buttonContinue: ButtonState,
)
data class SegmentSeedState(
val seedSegments: PersistentList<SegmentSeedType>,
val selectedSeedType: SegmentSeedType,
val onSelectType: (SegmentSeedType) -> Unit,
)
enum class SegmentSeedType(val count: Int) {
SEED_12(count = 12), SEED_24(count = 24);
fun toMnemonicType(): MnemonicType {
return when (this) {
SEED_12 -> MnemonicType.Mnemonic12
SEED_24 -> MnemonicType.Mnemonic24
}
}
companion object {
fun fromMnemonicType(mnemonicType: MnemonicType): SegmentSeedType {
return when (mnemonicType) {
MnemonicType.Mnemonic12 -> SEED_12
MnemonicType.Mnemonic24 -> SEED_24
}
}
}
}
data class MnemonicGridItem(
val index: Int,
val mnemonic: String,
)
data class CheckSeedPhraseState(
val tvSecondPhrase: TextFieldState,
val tvSeventhPhrase: TextFieldState,
val tvEleventhPhrase: TextFieldState,
val buttonCreateWallet: ButtonState,
)
data class ImportSeedPhraseState(
val fieldSeedPhrase: TextFieldState,
val fieldPassphrase: TextFieldState,
val onSuggestedPhraseClick: (Int) -> Unit,
val onPassphraseInfoClick: () -> Unit,
val buttonCreateWallet: ButtonState,
val invalidWords: Set<String> = emptySet(),
val suggestionsList: ImmutableList<String> = persistentListOf(),
val error: SeedPhraseError? = null,
val bottomSheetConfig: TangemBottomSheetConfig? = null,
)
data class ShowPassphraseInfoBottomSheetContent(val onOkClick: () -> Unit) : TangemBottomSheetConfigContent
data class TextFieldState(
val onTextFieldValueChanged: (TextFieldValue) -> Unit,
val textFieldValue: TextFieldValue = TextFieldValue(),
val label: String? = null,
val isError: Boolean = false,
val isFocused: Boolean = false,
val onFocusChanged: (Boolean) -> Unit = {},
)

View file

@ -1,60 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.model
import androidx.compose.ui.text.input.TextFieldValue
/**
[REDACTED_AUTHOR]
*/
data class UiActions(
val introActions: IntroUiAction,
val aboutActions: AboutUiAction,
val yourSeedPhraseActions: YourSeedPhraseUiAction,
val checkSeedPhraseActions: CheckSeedPhraseUiAction,
val importSeedPhraseActions: ImportSeedPhraseUiAction,
val menuChatClick: () -> Unit,
val menuNavigateBackClick: () -> Unit,
)
data class IntroUiAction(
val buttonCreateWalletClick: () -> Unit,
val buttonOtherOptionsClick: () -> Unit,
)
data class AboutUiAction(
val buttonReadMoreAboutSeedPhraseClick: () -> Unit,
val buttonGenerateSeedPhraseClick: () -> Unit,
val buttonImportSeedPhraseClick: () -> Unit,
)
data class YourSeedPhraseUiAction(
val buttonContinueClick: () -> Unit,
val onSelectType: (SegmentSeedType) -> Unit,
)
data class CheckSeedPhraseUiAction(
val secondTextFieldAction: TextFieldUiAction,
val seventhTextFieldAction: TextFieldUiAction,
val eleventhTextFieldAction: TextFieldUiAction,
val buttonCreateWalletClick: () -> Unit,
)
data class ImportSeedPhraseUiAction(
val phraseTextFieldAction: TextFieldUiAction,
val passTextFieldAction: TextFieldUiAction,
val suggestedPhraseClick: (Int) -> Unit,
val onPassphraseInfoClick: () -> Unit,
val buttonCreateWalletClick: () -> Unit,
)
data class TextFieldUiAction(
val onTextFieldChanged: (TextFieldValue) -> Unit,
val onFocusChanged: (Boolean) -> Unit = {},
)
enum class SeedPhraseField(val index: Int) {
Second(index = 1),
Seventh(index = 6),
Eleventh(index = 10),
}

View file

@ -1,151 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.AboutState
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
/**
[REDACTED_AUTHOR]
*/
@Composable
fun AboutSeedPhraseScreen(state: AboutState, modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxSize(),
) {
Box(
modifier = Modifier.weight(weight = 0.8f),
) {
EditIconBlock()
}
Box(
modifier = Modifier.weight(weight = 1.2f),
) {
Column {
OnboardingDescriptionBlock {
Description(
titleRes = R.string.onboarding_seed_intro_title,
subTitleRes = R.string.onboarding_seed_intro_message,
)
}
ReadMoreBlock(state)
}
}
Box {
OnboardingActionBlock(
firstActionContent = {
SecondaryButton(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.onboarding_seed_intro_button_generate),
enabled = state.buttonGenerateSeedPhrase.enabled,
showProgress = state.buttonGenerateSeedPhrase.showProgress,
onClick = state.buttonGenerateSeedPhrase.onClick,
)
},
secondActionContent = {
SecondaryButton(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.onboarding_seed_intro_button_import),
enabled = state.buttonImportSeedPhrase.enabled,
showProgress = state.buttonImportSeedPhrase.showProgress,
onClick = state.buttonImportSeedPhrase.onClick,
)
},
)
}
}
}
@Composable
private fun EditIconBlock() {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Image(
modifier = Modifier
.size(TangemTheme.dimens.size44),
painter = painterResource(id = R.drawable.ic_onboarding_text_edit_56),
contentDescription = null,
)
SpacerH32()
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.icon.warning.copy(alpha = 0.12f),
shape = RoundedCornerShape(TangemTheme.dimens.radius8),
),
) {
Text(
modifier = Modifier
.padding(
horizontal = TangemTheme.dimens.size12,
vertical = TangemTheme.dimens.size4,
),
text = stringResourceSafe(id = R.string.onboarding_seed_phrase_intro_legacy),
color = TangemTheme.colors.text.warning,
style = TangemTheme.typography.body1,
)
}
}
}
@Composable
private fun ReadMoreBlock(state: AboutState) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.size16),
) {
val shape = TangemTheme.shapes.roundedCornersLarge
Row(
modifier = Modifier
.align(Alignment.Center)
.clip(shape)
.border(
width = TangemTheme.dimens.size1,
color = TangemTheme.colors.stroke.primary,
shape = shape,
)
.clickable(onClick = state.buttonReadMoreAboutSeedPhrase.onClick)
.padding(
horizontal = TangemTheme.dimens.size16,
vertical = TangemTheme.dimens.size8,
),
) {
Icon(
painter = painterResource(id = R.drawable.ic_arrow_top_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
SpacerW8()
Text(
text = stringResourceSafe(id = R.string.onboarding_seed_button_read_more),
color = TangemTheme.colors.text.primary1,
)
}
}
}

View file

@ -1,110 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.core.ui.R
import com.tangem.core.ui.components.OutlineTextField
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.fields.contextmenu.DisableContextMenu
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.presentation.wallet2.model.CheckSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
/**
[REDACTED_AUTHOR]
*/
@Suppress("ReusedModifierInstance")
@Composable
fun CheckSeedPhraseScreen(state: CheckSeedPhraseState, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize(),
) {
LazyColumn(
modifier = modifier
.weight(1f)
.fillMaxWidth(),
) {
item {
OnboardingDescriptionBlock(
modifier = Modifier
.weight(1f)
.padding(top = TangemTheme.dimens.size48)
.padding(horizontal = TangemTheme.dimens.size16),
) {
Description(
titleRes = R.string.onboarding_seed_user_validation_title,
subTitleRes = R.string.onboarding_seed_user_validation_message,
)
}
}
item {
CheckSeedPhraseBlock(
modifier = Modifier
.imePadding()
.weight(1f)
.padding(top = TangemTheme.dimens.size20)
.padding(horizontal = TangemTheme.dimens.size16),
state = state,
)
}
}
OnboardingActionBlock(
firstActionContent = {
PrimaryButtonIconEnd(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.onboarding_create_wallet_button_create_wallet),
iconResId = R.drawable.ic_tangem_24,
enabled = state.buttonCreateWallet.enabled,
showProgress = state.buttonCreateWallet.showProgress,
onClick = state.buttonCreateWallet.onClick,
)
},
)
}
}
@Composable
private fun CheckSeedPhraseBlock(state: CheckSeedPhraseState, modifier: Modifier = Modifier) {
Column(modifier) {
CheckSeedPhraseTextField(state = state.tvSecondPhrase, imeAction = ImeAction.Next)
CheckSeedPhraseTextField(state = state.tvSeventhPhrase, imeAction = ImeAction.Next)
CheckSeedPhraseTextField(state = state.tvEleventhPhrase, imeAction = ImeAction.Done)
}
}
@Composable
private fun CheckSeedPhraseTextField(state: TextFieldState, imeAction: ImeAction) {
val focusManager = LocalFocusManager.current
DisableContextMenu {
OutlineTextField(
value = state.textFieldValue,
onValueChange = state.onTextFieldValueChanged,
label = state.label,
isError = state.isError,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
imeAction = imeAction,
),
keyboardActions = KeyboardActions(
onDone = { focusManager.clearFocus() },
onNext = { focusManager.moveFocus(focusDirection = FocusDirection.Down) },
),
)
}
}

View file

@ -1,268 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState
import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.InvalidWordsColorTransformation
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseErrorConverter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
[REDACTED_AUTHOR]
*/
@Composable
fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
val keyboard by keyboardAsState()
Box(
modifier = modifier.fillMaxSize(),
) {
Column {
OnboardingDescriptionBlock(
modifier = Modifier.padding(top = TangemTheme.dimens.size16),
) {
DescriptionSubTitleText(text = stringResourceSafe(id = R.string.onboarding_seed_import_message))
}
PhraseBlock(
modifier = Modifier
.padding(top = TangemTheme.dimens.size62)
.padding(horizontal = TangemTheme.dimens.size16),
state = state,
)
PassphraseBlock(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.size16),
passphraseField = state.fieldPassphrase,
onPassphraseInfoClick = state.onPassphraseInfoClick,
)
OnboardingActionBlock(
modifier = Modifier.padding(top = TangemTheme.dimens.size16),
firstActionContent = {
PrimaryButtonIconEnd(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.common_import),
iconResId = R.drawable.ic_tangem_24,
enabled = state.buttonCreateWallet.enabled,
showProgress = state.buttonCreateWallet.showProgress,
onClick = state.buttonCreateWallet.onClick,
)
},
)
}
if (keyboard is Keyboard.Opened) {
SuggestionsBlock(
modifier = Modifier
.align(Alignment.BottomCenter)
.imePadding(),
suggestionsList = state.suggestionsList,
onClick = { index -> state.onSuggestedPhraseClick(index) },
)
}
if (state.bottomSheetConfig != null) {
PassphraseInfoBottomSheet(config = state.bottomSheetConfig)
}
}
}
@Composable
private fun PassphraseBlock(
passphraseField: TextFieldState,
onPassphraseInfoClick: () -> Unit,
modifier: Modifier = Modifier,
) {
OutlineTextFieldWithIcon(
modifier = modifier.fillMaxWidth(),
value = passphraseField.textFieldValue,
onValueChange = passphraseField.onTextFieldValueChanged,
iconResId = R.drawable.ic_information_24,
iconColor = TangemTheme.colors.icon.informative,
label = stringResourceSafe(id = R.string.common_passphrase),
placeholder = stringResourceSafe(id = R.string.send_optional_field),
onIconClick = onPassphraseInfoClick,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
),
)
}
@Composable
private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
// TODO: replace by TextReference
val errorConverter = remember { SeedPhraseErrorConverter() }
Column(
modifier = modifier.fillMaxWidth(),
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size142),
value = state.fieldSeedPhrase.textFieldValue,
onValueChange = state.fieldSeedPhrase.onTextFieldValueChanged,
textStyle = TangemTheme.typography.body1,
singleLine = false,
visualTransformation = InvalidWordsColorTransformation(
wordsToBrush = state.invalidWords,
style = SpanStyle(color = TangemTheme.colors.text.warning),
),
colors = TangemTextFieldsDefault.defaultTextFieldColors,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
),
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size32),
) {
val message = errorConverter.convert(LocalContext.current to state.error)
if (message != null) {
Text(
modifier = Modifier.fillMaxSize(),
text = message,
style = TangemTheme.typography.caption2.copy(
color = TangemTheme.colors.text.warning,
),
)
}
}
}
}
@Suppress("ReusedModifierInstance")
@Composable
private fun SuggestionsBlock(
suggestionsList: ImmutableList<String>,
onClick: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
modifier = modifier,
enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }),
exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(),
visible = suggestionsList.isNotEmpty(),
) {
LazyRow(
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16),
) {
items(suggestionsList.size) { index ->
SuggestionButton(
modifier = Modifier.rowPadding(
index = index,
rowSize = suggestionsList.size,
outSide = TangemTheme.dimens.size0,
inSide = TangemTheme.dimens.size4,
),
text = suggestionsList[index],
onClick = { onClick(index) },
)
}
}
}
}
@Composable
private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.clickable(onClick = onClick)) {
Notifier(
text = text,
backgroundColor = TangemTheme.colors.icon.primary1,
textColor = TangemTheme.colors.text.primary2,
)
}
}
private fun Modifier.rowPadding(index: Int, rowSize: Int, outSide: Dp, inSide: Dp): Modifier = when (index) {
0 -> this.padding(start = outSide, end = inSide)
rowSize - 1 -> this.padding(start = inSide, end = outSide)
else -> this.padding(horizontal = inSide)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SuggestionsBlockPreview(
@PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList<String>,
) {
TangemThemePreview {
SuggestionsBlock(
suggestionsList = suggestions,
onClick = {},
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ImportSeedPhraseScreenPreview(
@PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList<String>,
) {
TangemThemePreview {
Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
ImportSeedPhraseScreen(
ImportSeedPhraseState(
fieldSeedPhrase = TextFieldState(onTextFieldValueChanged = {}),
fieldPassphrase = TextFieldState(onTextFieldValueChanged = {}),
onSuggestedPhraseClick = {},
onPassphraseInfoClick = {},
buttonCreateWallet = ButtonState(
enabled = true,
isClickable = true,
showProgress = false,
onClick = {},
),
suggestionsList = suggestions,
),
)
}
}
}
private class SuggestionsPreviewParamsProvider : CollectionPreviewParameterProvider<ImmutableList<String>>(
collection = listOf(
persistentListOf(
"one",
"each",
"explorer",
"unknown",
),
),
)

View file

@ -1,102 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.IntroState
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
/**
[REDACTED_AUTHOR]
*/
@Composable
fun IntroScreen(state: IntroState, modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxSize(),
) {
Box(
modifier = Modifier.weight(weight = 1.5f),
) {
CardImageBlock(state.cardImageUrl)
}
Box(
modifier = Modifier.weight(weight = 1f),
) {
OnboardingDescriptionBlock {
Description(
titleRes = R.string.onboarding_create_wallet_options_title,
subTitleRes = R.string.onboarding_create_wallet_options_message,
)
}
}
Box {
OnboardingActionBlock(
firstActionContent = {
PrimaryButtonIconEnd(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.onboarding_create_wallet_button_create_wallet),
iconResId = R.drawable.ic_tangem_24,
enabled = state.buttonCreateWallet.enabled,
showProgress = state.buttonCreateWallet.showProgress,
onClick = state.buttonCreateWallet.onClick,
)
},
secondActionContent = {
SecondaryButton(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.onboarding_create_wallet_options_button_options),
enabled = state.buttonOtherOptions.enabled,
showProgress = state.buttonOtherOptions.showProgress,
onClick = state.buttonOtherOptions.onClick,
)
},
)
}
}
}
@Composable
private fun CardImageBlock(cardImageUrl: String? = null) {
Box(modifier = Modifier.fillMaxSize()) {
val cardImageModifier = Modifier
.fillMaxSize()
.align(Alignment.Center)
SubcomposeAsyncImage(
modifier = cardImageModifier.padding(horizontal = TangemTheme.dimens.size34),
model = ImageRequest.Builder(LocalContext.current)
.data(cardImageUrl)
.crossfade(true)
.build(),
loading = { CardPlaceHolder(cardImageModifier) },
error = { CardPlaceHolder(cardImageModifier) },
contentDescription = null,
)
}
}
@Composable
private fun CardPlaceHolder(modifier: Modifier = Modifier) {
Image(
modifier = modifier,
painter = painterResource(id = R.drawable.card_placeholder_black),
contentScale = ContentScale.Fit,
contentDescription = null,
)
}

View file

@ -1,90 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent
@Composable
fun PassphraseInfoBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
containerColor = TangemTheme.colors.background.primary,
) { content: ShowPassphraseInfoBottomSheetContent ->
PassphraseInfoBottomSheetContent(content)
}
}
@Composable
fun PassphraseInfoBottomSheetContent(content: ShowPassphraseInfoBottomSheetContent) {
Column(
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.fillMaxWidth(),
) {
Icon(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = TangemTheme.dimens.size40)
.size(TangemTheme.dimens.size48),
painter = painterResource(id = R.drawable.ic_information_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
Text(
text = stringResourceSafe(id = R.string.common_passphrase),
modifier = Modifier
.padding(top = TangemTheme.dimens.size40)
.align(Alignment.CenterHorizontally),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h2,
)
Text(
text = stringResourceSafe(id = R.string.onboarding_bottom_sheet_passphrase_description),
modifier = Modifier
.padding(top = TangemTheme.dimens.size16)
.padding(horizontal = TangemTheme.dimens.size24)
.align(Alignment.CenterHorizontally),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
textAlign = TextAlign.Center,
)
PrimaryButton(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.size16)
.padding(top = TangemTheme.dimens.size40)
.padding(bottom = TangemTheme.dimens.size32)
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.common_ok),
onClick = content.onOkClick,
)
}
}
@Preview
@Composable
private fun PassphraseInfoBottomSheetContentPreview() {
TangemThemePreview {
PassphraseInfoBottomSheetContent(ShowPassphraseInfoBottomSheetContent { })
}
}

View file

@ -1,196 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.extensions.pluralStringResourceSafe
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.presentation.wallet2.model.*
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionTitleText
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
/**
[REDACTED_AUTHOR]
*/
@Composable
fun YourSeedPhraseScreen(state: YourSeedPhraseState, modifier: Modifier = Modifier) {
Box(
modifier = modifier.fillMaxSize(),
) {
Column(
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing92)
.verticalScroll(rememberScrollState())
.fillMaxSize(),
) {
SegmentSeedBlock(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing20),
state = state.segmentSeedState,
)
TitleBlock(state.segmentSeedState)
SeedPhraseGridBlock(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing20)
.padding(horizontal = TangemTheme.dimens.size16),
mnemonicGridItems = state.mnemonicGridItems,
)
}
OnboardingActionBlock(
modifier = Modifier.align(Alignment.BottomCenter),
firstActionContent = {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(id = R.string.common_continue),
onClick = state.buttonContinue.onClick,
)
},
)
}
}
@Composable
private fun TitleBlock(state: SegmentSeedState) {
Box(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.size36)
.padding(top = TangemTheme.dimens.spacing20)
.fillMaxWidth(),
) {
Column {
DescriptionTitleText(text = stringResourceSafe(id = R.string.onboarding_seed_generate_title))
SpacerH16()
DescriptionSubTitleText(
text = pluralStringResourceSafe(
id = R.plurals.onboarding_seed_generate_message_words_count,
count = state.selectedSeedType.count,
state.selectedSeedType.count,
),
)
}
}
}
@Composable
private fun SegmentSeedBlock(state: SegmentSeedState, modifier: Modifier = Modifier) {
SegmentedButtons(
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing76),
config = state.seedSegments,
initialSelectedItem = state.selectedSeedType,
onClick = {
state.onSelectType(it)
},
) {
Text(
text = pluralStringResourceSafe(
id = R.plurals.onboarding_seed_generate_words_count,
count = it.count,
it.count,
),
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing10)
.fillMaxWidth(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun SeedPhraseGridBlock(mnemonicGridItems: ImmutableList<MnemonicGridItem>, modifier: Modifier = Modifier) {
VerticalGrid(
modifier = modifier,
items = mnemonicGridItems,
columns = 2,
) { item ->
Row(
modifier = Modifier.padding(all = TangemTheme.dimens.size8),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.width(TangemTheme.dimens.size40),
text = "${item.index}.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
Text(
text = item.mnemonic,
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
}
}
}
@Composable
private inline fun <T> VerticalGrid(
items: ImmutableList<T>,
columns: Int,
modifier: Modifier = Modifier,
crossinline content: @Composable (T) -> Unit,
) {
val columnLength = items.size / columns
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
for (index in 0 until columns) {
Column {
for (i in 0 until columnLength) {
val item = items[index * columnLength + i]
content(item)
}
}
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun YourSeedPhraseScreenPreview_Light() {
TangemThemePreview {
YourSeedPhraseScreen(
state = YourSeedPhraseState(
segmentSeedState = SegmentSeedState(
seedSegments = persistentListOf(
SegmentSeedType.SEED_12,
SegmentSeedType.SEED_24,
),
selectedSeedType = SegmentSeedType.SEED_24,
onSelectType = {},
),
mnemonicGridItems = (1..24).map { MnemonicGridItem(it, it.toString()) }.toPersistentList(),
buttonContinue = ButtonState(
enabled = true,
isClickable = true,
showProgress = false,
onClick = {},
),
),
modifier = Modifier
.fillMaxSize()
.background(color = TangemTheme.colors.background.primary),
)
}
}

View file

@ -1,39 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.res.TangemTheme
/**
[REDACTED_AUTHOR]
*/
@Composable
fun OnboardingActionBlock(
modifier: Modifier = Modifier,
firstActionContent: @Composable (() -> Unit)? = null,
secondActionContent: @Composable (() -> Unit)? = null,
) {
if (firstActionContent == null && secondActionContent == null) return
Box(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.size16)
.padding(
top = TangemTheme.dimens.size8,
bottom = TangemTheme.dimens.size32,
),
) {
Column(
modifier = Modifier.align(Alignment.BottomCenter),
) {
firstActionContent?.invoke()
if (firstActionContent != null && secondActionContent != null) SpacerH12()
secondActionContent?.invoke()
}
}
}

View file

@ -1,64 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui.components
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
/**
* UI component witch provides single and carousel description for the onboarding process
*/
@Composable
fun OnboardingDescriptionBlock(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Box(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.size36),
) {
content()
}
}
@Composable
fun Description(@StringRes titleRes: Int, @StringRes subTitleRes: Int) {
Column {
DescriptionTitleText(text = stringResourceSafe(id = titleRes))
SpacerH16()
DescriptionSubTitleText(text = stringResourceSafe(id = subTitleRes))
}
}
@Composable
fun DescriptionTitleText(text: String) {
Text(
text = text,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
@Composable
fun DescriptionSubTitleText(text: String) {
// the text style made similar to TextViewOnboarding.Body
Text(
text = text,
style = TangemTheme.typography.body1.copy(
lineHeight = 20.sp,
letterSpacing = 0.03.sp,
),
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}

View file

@ -1,77 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField
/**
[REDACTED_AUTHOR]
*/
class CheckSeedPhraseStateBuilder {
fun updateTextField(
uiState: OnboardingSeedPhraseState,
field: SeedPhraseField,
textFieldValue: TextFieldValue,
): OnboardingSeedPhraseState = when (field) {
SeedPhraseField.Second -> uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvSecondPhrase = uiState.checkSeedPhraseState.tvSecondPhrase.copy(
textFieldValue = textFieldValue,
),
),
)
SeedPhraseField.Seventh -> uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvSeventhPhrase = uiState.checkSeedPhraseState.tvSeventhPhrase.copy(
textFieldValue = textFieldValue,
),
),
)
SeedPhraseField.Eleventh -> uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvEleventhPhrase = uiState.checkSeedPhraseState.tvEleventhPhrase.copy(
textFieldValue = textFieldValue,
),
),
)
}
fun updateTextFieldError(
uiState: OnboardingSeedPhraseState,
field: SeedPhraseField,
hasError: Boolean,
): OnboardingSeedPhraseState = when (field) {
SeedPhraseField.Second -> uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvSecondPhrase = uiState.checkSeedPhraseState.tvSecondPhrase.copy(
isError = hasError,
),
),
)
SeedPhraseField.Seventh -> uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvSeventhPhrase = uiState.checkSeedPhraseState.tvSeventhPhrase.copy(
isError = hasError,
),
),
)
SeedPhraseField.Eleventh -> uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvEleventhPhrase = uiState.checkSeedPhraseState.tvEleventhPhrase.copy(
isError = hasError,
),
),
)
}
fun updateCreateWalletButton(uiState: OnboardingSeedPhraseState, enabled: Boolean): OnboardingSeedPhraseState {
return uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
buttonCreateWallet = uiState.checkSeedPhraseState.buttonCreateWallet.copy(
enabled = enabled,
),
),
)
}
}

View file

@ -1,108 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.feature.onboarding.domain.InsertSuggestionResult
import com.tangem.feature.onboarding.domain.SeedPhraseError
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent
import kotlinx.collections.immutable.ImmutableList
/**
[REDACTED_AUTHOR]
*/
class ImportSeedPhraseStateBuilder {
fun updateSeedPhraseTextField(
uiState: OnboardingSeedPhraseState,
textFieldValue: TextFieldValue,
): OnboardingSeedPhraseState = uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
fieldSeedPhrase = uiState.importSeedPhraseState.fieldSeedPhrase.copy(
textFieldValue = textFieldValue,
),
),
)
fun updatePassPhraseTextField(
uiState: OnboardingSeedPhraseState,
textFieldValue: TextFieldValue,
): OnboardingSeedPhraseState = uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
fieldPassphrase = uiState.importSeedPhraseState.fieldPassphrase.copy(
textFieldValue = textFieldValue,
),
),
)
fun updateCreateWalletButton(uiState: OnboardingSeedPhraseState, enabled: Boolean): OnboardingSeedPhraseState {
return uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
buttonCreateWallet = uiState.importSeedPhraseState.buttonCreateWallet.copy(
enabled = enabled,
),
),
)
}
fun updateInvalidWords(uiState: OnboardingSeedPhraseState, invalidWords: Set<String>): OnboardingSeedPhraseState =
uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
invalidWords = invalidWords,
),
)
fun updateSuggestions(
uiState: OnboardingSeedPhraseState,
suggestions: ImmutableList<String>,
): OnboardingSeedPhraseState = uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
suggestionsList = suggestions,
),
)
fun showPassphraseInfoBottomSheet(
uiState: OnboardingSeedPhraseState,
onDismiss: () -> Unit,
): OnboardingSeedPhraseState {
return uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = onDismiss,
content = ShowPassphraseInfoBottomSheetContent(onDismiss),
),
),
)
}
fun dismissPassphraseBottomSheet(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState {
return uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
bottomSheetConfig = uiState.importSeedPhraseState.bottomSheetConfig?.copy(
isShown = false,
),
),
)
}
fun insertSuggestionWord(
uiState: OnboardingSeedPhraseState,
insertResult: InsertSuggestionResult,
): OnboardingSeedPhraseState {
val newTextFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue.copy(
text = insertResult.text,
selection = TextRange(insertResult.cursorPosition, insertResult.cursorPosition),
)
return updateSeedPhraseTextField(uiState, newTextFieldValue)
}
fun updateError(uiState: OnboardingSeedPhraseState, error: SeedPhraseError?): OnboardingSeedPhraseState {
return uiState.copy(
importSeedPhraseState = uiState.importSeedPhraseState.copy(
error = error,
),
)
}
}

View file

@ -1,197 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
import com.tangem.feature.onboarding.presentation.wallet2.model.*
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
/**
[REDACTED_AUTHOR]
*/
class StateBuilder(
private val uiActions: UiActions,
) {
val checkSeedPhrase: CheckSeedPhraseStateBuilder = CheckSeedPhraseStateBuilder()
val importSeedPhrase: ImportSeedPhraseStateBuilder = ImportSeedPhraseStateBuilder()
fun init(selectedSeedType: SegmentSeedType): OnboardingSeedPhraseState = OnboardingSeedPhraseState(
introState = getInitialIntroState(),
aboutState = getInitialAboutState(),
yourSeedPhraseState = getInitialYourSeedPhraseState(selectedSeedType),
checkSeedPhraseState = getInitialCheckSeedPhraseState(),
importSeedPhraseState = getInitialImportSeedPhraseState(),
menuButtonChat = ButtonState(
onClick = uiActions.menuChatClick,
),
onBackClick = uiActions.menuNavigateBackClick,
)
private fun getInitialImportSeedPhraseState(): ImportSeedPhraseState {
return ImportSeedPhraseState(
fieldSeedPhrase = TextFieldState(
onTextFieldValueChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onFocusChanged,
),
fieldPassphrase = TextFieldState(
onTextFieldValueChanged = uiActions.importSeedPhraseActions.passTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.importSeedPhraseActions.passTextFieldAction.onFocusChanged,
),
onSuggestedPhraseClick = uiActions.importSeedPhraseActions.suggestedPhraseClick,
onPassphraseInfoClick = uiActions.importSeedPhraseActions.onPassphraseInfoClick,
buttonCreateWallet = ButtonState(
enabled = false,
onClick = uiActions.importSeedPhraseActions.buttonCreateWalletClick,
),
)
}
private fun getInitialCheckSeedPhraseState(): CheckSeedPhraseState {
return CheckSeedPhraseState(
tvSecondPhrase = TextFieldState(
label = "2",
isFocused = true,
onTextFieldValueChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onFocusChanged,
),
tvSeventhPhrase = TextFieldState(
label = "7",
isFocused = false,
onTextFieldValueChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onFocusChanged,
),
tvEleventhPhrase = TextFieldState(
label = "11",
isFocused = false,
onTextFieldValueChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onFocusChanged,
),
buttonCreateWallet = ButtonState(
enabled = false,
onClick = uiActions.checkSeedPhraseActions.buttonCreateWalletClick,
),
)
}
private fun getInitialYourSeedPhraseState(selectedSeedType: SegmentSeedType): YourSeedPhraseState {
return YourSeedPhraseState(
segmentSeedState = SegmentSeedState(
seedSegments = persistentListOf(
SegmentSeedType.SEED_12,
SegmentSeedType.SEED_24,
),
selectedSeedType = selectedSeedType,
onSelectType = uiActions.yourSeedPhraseActions.onSelectType,
),
buttonContinue = ButtonState(
onClick = uiActions.yourSeedPhraseActions.buttonContinueClick,
),
)
}
private fun getInitialAboutState(): AboutState {
return AboutState(
buttonReadMoreAboutSeedPhrase = ButtonState(
onClick = uiActions.aboutActions.buttonReadMoreAboutSeedPhraseClick,
),
buttonGenerateSeedPhrase = ButtonState(
onClick = uiActions.aboutActions.buttonGenerateSeedPhraseClick,
),
buttonImportSeedPhrase = ButtonState(
onClick = uiActions.aboutActions.buttonImportSeedPhraseClick,
),
)
}
private fun getInitialIntroState(): IntroState {
return IntroState(
buttonCreateWallet = ButtonState(
onClick = uiActions.introActions.buttonCreateWalletClick,
),
buttonOtherOptions = ButtonState(
onClick = uiActions.introActions.buttonOtherOptionsClick,
),
)
}
fun setCardArtwork(uiState: OnboardingSeedPhraseState, cardArtwork: String): OnboardingSeedPhraseState =
uiState.copy(
introState = uiState.introState.copy(
cardImageUrl = cardArtwork,
),
)
fun generateMnemonicComponents(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState {
return uiState.copy(
aboutState = uiState.aboutState.copy(
buttonGenerateSeedPhrase = uiState.aboutState.buttonGenerateSeedPhrase.copy(
showProgress = true,
),
buttonImportSeedPhrase = uiState.aboutState.buttonImportSeedPhrase.copy(
enabled = false,
),
),
)
}
fun mnemonicGenerated(
uiState: OnboardingSeedPhraseState,
mnemonicGridItems: PersistentList<MnemonicGridItem>,
): OnboardingSeedPhraseState {
return updateMnemonicComponents(uiState, mnemonicGridItems)
.copy(
aboutState = uiState.aboutState.copy(
buttonGenerateSeedPhrase = uiState.aboutState.buttonGenerateSeedPhrase.copy(
showProgress = false,
),
buttonImportSeedPhrase = uiState.aboutState.buttonImportSeedPhrase.copy(
enabled = true,
),
),
)
}
fun selectSeedType(
uiState: OnboardingSeedPhraseState,
mnemonicGridItems: PersistentList<MnemonicGridItem>,
selectedSeedType: SegmentSeedType,
): OnboardingSeedPhraseState = uiState.copy(
yourSeedPhraseState = uiState.yourSeedPhraseState.copy(
mnemonicGridItems = mnemonicGridItems,
segmentSeedState = uiState.yourSeedPhraseState.segmentSeedState.copy(
selectedSeedType = selectedSeedType,
),
),
)
fun clearCheckSeedPhraseState(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState = uiState.copy(
checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
tvSecondPhrase = TextFieldState(
label = "2",
isFocused = true,
onTextFieldValueChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onFocusChanged,
),
tvSeventhPhrase = TextFieldState(
label = "7",
isFocused = false,
onTextFieldValueChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onFocusChanged,
),
tvEleventhPhrase = TextFieldState(
label = "11",
isFocused = false,
onTextFieldValueChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onTextFieldChanged,
onFocusChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onFocusChanged,
),
),
)
private fun updateMnemonicComponents(
uiState: OnboardingSeedPhraseState,
mnemonicGridItems: PersistentList<MnemonicGridItem>,
): OnboardingSeedPhraseState = uiState.copy(
yourSeedPhraseState = uiState.yourSeedPhraseState.copy(
mnemonicGridItems = mnemonicGridItems,
),
)
}

View file

@ -1,79 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import com.tangem.utils.extensions.isSingleItem
/**
[REDACTED_AUTHOR]
*/
class InvalidWordsColorTransformation(
private val wordsToBrush: Set<String>,
private val style: SpanStyle,
) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val plainText = text.text
if (wordsToBrush.isSingleItem() && wordsToBrush.first() == plainText) {
return plainText.annotate().toTransformedText()
}
val wordList = makeSeparatedWordlist(plainText)
val annotatedString = makeAnnotatedString(wordList)
return annotatedString.toTransformedText()
}
private fun makeSeparatedWordlist(plainText: String): List<String> {
val plainTextCharArray = plainText.toCharArray()
val wordlist = mutableListOf<String>()
var wordBuilder = StringBuilder()
for (index in plainTextCharArray.indices) {
val char = plainTextCharArray[index]
if (char.isWhitespace()) {
if (wordBuilder.isEmpty()) {
wordlist.add(WORD_SEPARATOR)
} else {
wordlist.add(wordBuilder.toString())
wordBuilder = StringBuilder()
wordlist.add(WORD_SEPARATOR)
}
} else {
if (index == plainTextCharArray.lastIndex) {
wordBuilder.append(char)
wordlist.add(wordBuilder.toString())
wordBuilder = StringBuilder()
} else {
wordBuilder.append(char)
}
}
}
return wordlist.toList()
}
private fun makeAnnotatedString(wordList: List<String>): AnnotatedString = buildAnnotatedString {
wordList.forEach {
if (it == WORD_SEPARATOR) {
append(WHITE_SPACE)
} else if (wordsToBrush.contains(it)) {
append(it.annotate())
} else {
append(it)
}
}
}
private fun String.annotate(): AnnotatedString = AnnotatedString(this, style)
private fun AnnotatedString.toTransformedText(): TransformedText = TransformedText(this, OffsetMapping.Identity)
companion object {
private const val WHITE_SPACE = " "
private const val WORD_SEPARATOR = "_$@-*"
}
}

View file

@ -1,28 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
import android.content.Context
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.feature.onboarding.R
import com.tangem.feature.onboarding.domain.SeedPhraseError
/**
[REDACTED_AUTHOR]
*/
class SeedPhraseErrorConverter : ModuleMessageConverter<Pair<Context, SeedPhraseError?>, String?> {
override fun convert(source: Pair<Context, SeedPhraseError?>): String? {
val (context, message) = source
val convertedMessage = when (message) {
SeedPhraseError.InvalidChecksum -> {
context.getString(R.string.onboarding_seed_mnemonic_invalid_checksum)
}
is SeedPhraseError.InvalidWords -> {
context.getString(R.string.onboarding_seed_mnemonic_wrong_words)
}
else -> null
}
return convertedMessage
}
}

View file

@ -1,22 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
import com.tangem.common.CompletionResult
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
/**
[REDACTED_AUTHOR]
*/
interface SeedPhraseMediator {
fun createWallet(callback: (CompletionResult<CreateWalletResponse>) -> Unit)
fun importWallet(
mnemonicComponents: List<String>,
passphrase: String?,
seedPhraseSource: SeedPhraseSource,
callback: (CompletionResult<CreateWalletResponse>) -> Unit,
)
fun onWalletCreated(result: CompletionResult<CreateWalletResponse>)
fun allowScreenshots(allow: Boolean)
}

View file

@ -1,50 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
import android.net.Uri
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
[REDACTED_AUTHOR]
*/
class SeedPhraseRouter(
private val onBack: () -> Unit,
private val onOpenChat: () -> Unit,
private val onOpenUriClick: (Uri) -> Unit,
) {
private val _currentScreen = MutableStateFlow(SeedPhraseScreen.Intro)
val currentScreen: StateFlow<SeedPhraseScreen>
get() = _currentScreen
fun navigateBack() {
_currentScreen.value = when (_currentScreen.value) {
SeedPhraseScreen.CheckSeedPhrase -> SeedPhraseScreen.YourSeedPhrase
SeedPhraseScreen.AboutSeedPhrase -> SeedPhraseScreen.Intro
SeedPhraseScreen.ImportSeedPhrase,
SeedPhraseScreen.YourSeedPhrase,
-> SeedPhraseScreen.AboutSeedPhrase
SeedPhraseScreen.Intro -> {
onBack.invoke()
return
}
}
}
fun openScreen(screen: SeedPhraseScreen) {
_currentScreen.value = screen
}
fun openChat() {
onOpenChat.invoke()
}
fun openUri(uri: Uri) {
onOpenUriClick.invoke(uri)
}
}
enum class SeedPhraseScreen {
Intro, AboutSeedPhrase, YourSeedPhrase, CheckSeedPhrase, ImportSeedPhrase
}

View file

@ -1,484 +0,0 @@
package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
import android.net.Uri
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.common.CompletionResult
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.common.locale.LocaleProvider
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.domain.SeedPhraseError
import com.tangem.feature.onboarding.domain.SeedPhraseInteractor
import com.tangem.feature.onboarding.domain.models.MnemonicType
import com.tangem.feature.onboarding.presentation.wallet2.analytics.CreateWalletEvents
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseEvents
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.feature.onboarding.presentation.wallet2.model.*
import com.tangem.feature.onboarding.presentation.wallet2.model.SegmentSeedType.Companion.fromMnemonicType
import com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders.StateBuilder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.Debouncer
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import javax.inject.Inject
/**
[REDACTED_AUTHOR]
*/
@Suppress("LargeClass")
@HiltViewModel
class SeedPhraseViewModel @Inject constructor(
private val interactor: SeedPhraseInteractor,
private val localeProvider: LocaleProvider,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel() {
private var uiBuilder = StateBuilder(uiActions = createUiActions())
private var selectedMnemonicType = MnemonicType.Mnemonic12
var uiState: OnboardingSeedPhraseState by mutableStateOf(uiBuilder.init(fromMnemonicType(selectedMnemonicType)))
private set
private lateinit var router: SeedPhraseRouter
private lateinit var mediator: SeedPhraseMediator
val currentScreen: StateFlow<SeedPhraseScreen>
get() = router.currentScreen
val progress: Flow<Int>
get() = currentScreen.map { progressByScreen[it] ?: 0 }
val maxProgress: Int by lazy { progressByScreen.maxOfOrNull { it.value } ?: 0 }
var isFinished: Boolean = false
private set
private val progressByScreen = mapOf(
SeedPhraseScreen.Intro to 1,
SeedPhraseScreen.AboutSeedPhrase to 2,
SeedPhraseScreen.YourSeedPhrase to 3,
SeedPhraseScreen.CheckSeedPhrase to 3,
SeedPhraseScreen.ImportSeedPhrase to 3,
)
private val textFieldsDebouncers = mutableMapOf<String, Debouncer>()
private var generatedMnemonicComponents: Map<MnemonicType, List<String>> = emptyMap()
private var importedMnemonicComponents: List<String>? = null
private var importedPassphrase: String? = null
override fun onCleared() {
textFieldsDebouncers.forEach { entry -> entry.value.release() }
textFieldsDebouncers.clear()
super.onCleared()
}
fun setRouter(router: SeedPhraseRouter) {
this.router = router
subscribeToScreenChanges()
}
private fun subscribeToScreenChanges() {
router.currentScreen.onEach { screen ->
when (screen) {
SeedPhraseScreen.Intro -> {
/* no-op */
}
SeedPhraseScreen.AboutSeedPhrase -> {
analyticsEventHandler.send(SeedPhraseEvents.IntroScreenOpened)
mediator.allowScreenshots(true)
}
SeedPhraseScreen.YourSeedPhrase -> {
analyticsEventHandler.send(SeedPhraseEvents.GenerationScreenOpened)
mediator.allowScreenshots(false)
}
SeedPhraseScreen.CheckSeedPhrase -> {
analyticsEventHandler.send(SeedPhraseEvents.CheckingScreenOpened)
mediator.allowScreenshots(true)
}
SeedPhraseScreen.ImportSeedPhrase -> {
analyticsEventHandler.send(SeedPhraseEvents.ImportScreenOpened)
}
}
}.launchIn(viewModelScope)
}
fun setMediator(mediator: SeedPhraseMediator) {
this.mediator = mediator
}
fun setCardArtworkUri(cardArtworkUri: String) {
if (uiState.introState.cardImageUrl != cardArtworkUri) {
uiState = uiBuilder.setCardArtwork(uiState, cardArtworkUri)
}
}
private fun createUiActions(): UiActions = UiActions(
introActions = IntroUiAction(
buttonCreateWalletClick = ::buttonCreateWalletClick,
buttonOtherOptionsClick = ::buttonOtherOptionsClick,
),
aboutActions = AboutUiAction(
buttonReadMoreAboutSeedPhraseClick = ::buttonReadMoreAboutSeedPhraseClick,
buttonGenerateSeedPhraseClick = ::buttonGenerateSeedPhraseClick,
buttonImportSeedPhraseClick = ::buttonImportSeedPhraseClick,
),
yourSeedPhraseActions = YourSeedPhraseUiAction(
buttonContinueClick = ::buttonContinueClick,
onSelectType = ::onSelectType,
),
checkSeedPhraseActions = CheckSeedPhraseUiAction(
buttonCreateWalletClick = {
val mnemonicComponents =
generatedMnemonicComponents[selectedMnemonicType] ?: return@CheckSeedPhraseUiAction
buttonImportWalletClick(mnemonicComponents, null, SeedPhraseSource.GENERATED)
},
secondTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Second, value) },
),
seventhTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Seventh, value) },
),
eleventhTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Eleventh, value) },
),
),
importSeedPhraseActions = ImportSeedPhraseUiAction(
phraseTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onSeedPhraseTextFieldChanged(value) },
),
passTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onPassPhraseTextFieldChanged(value) },
),
suggestedPhraseClick = ::buttonSuggestedPhraseClick,
onPassphraseInfoClick = ::buttonPassphraseInfoClick,
buttonCreateWalletClick = {
analyticsEventHandler.send(SeedPhraseEvents.ButtonImport)
buttonImportWalletClick(importedMnemonicComponents, importedPassphrase, SeedPhraseSource.IMPORTED)
},
),
menuChatClick = ::menuChatClick,
menuNavigateBackClick = ::menuNavigateBackClick,
)
// region CheckSeedPhrase
private fun onTextFieldChanged(field: SeedPhraseField, textFieldValue: TextFieldValue) {
launchSingle {
updateUi { uiBuilder.checkSeedPhrase.updateTextField(uiState, field, textFieldValue) }
val fieldState = field.getState(uiState)
if (fieldState.textFieldValue.text.isEmpty()) {
updateUi {
val mediate = uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError = false)
uiBuilder.checkSeedPhrase.updateCreateWalletButton(mediate, enabled = false)
}
return@launchSingle
}
createOrGetDebouncer(field.name).debounce(viewModelScope, context = dispatchers.io) {
val mnemonicComponents = generatedMnemonicComponents[selectedMnemonicType] ?: return@debounce
val hasError = !interactor.isWordMatch(mnemonicComponents, field, textFieldValue.text)
if (fieldState.isError != hasError) {
updateUi { uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError) }
}
val isCreateWalletButtonEnabled = SeedPhraseField.entries
.map { field -> field.getState(uiState) }
.all { fieldState -> fieldState.textFieldValue.text.isNotEmpty() && !fieldState.isError }
if (uiState.checkSeedPhraseState.buttonCreateWallet.enabled != isCreateWalletButtonEnabled) {
updateUi {
uiBuilder.checkSeedPhrase.updateCreateWalletButton(
uiState = uiState,
enabled = isCreateWalletButtonEnabled,
)
}
}
}
}
}
// endregion CheckSeedPhrase
// region ImportSeedPhrase
private fun onSeedPhraseTextFieldChanged(textFieldValue: TextFieldValue) {
val oldTextFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue
val isSameText = textFieldValue.text == oldTextFieldValue.text
val isCursorMoved = textFieldValue.selection != oldTextFieldValue.selection
uiState = uiBuilder.importSeedPhrase.updateSeedPhraseTextField(uiState, textFieldValue)
val fieldState = uiState.importSeedPhraseState.fieldSeedPhrase
val inputMnemonic = fieldState.textFieldValue.text
val debouncer = createOrGetDebouncer(MNEMONIC_DEBOUNCER)
when {
isSameText && isCursorMoved -> {
debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) {
updateSuggestions(fieldState)
}
}
isSameText && !isCursorMoved -> {
// do nothing
}
else -> {
debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) {
updateSuggestions(fieldState)
validateMnemonic(inputMnemonic)
}
}
}
}
private fun onPassPhraseTextFieldChanged(textFieldValue: TextFieldValue) {
importedPassphrase = textFieldValue.text
uiState = uiBuilder.importSeedPhrase.updatePassPhraseTextField(uiState, textFieldValue)
}
private suspend fun validateMnemonic(inputMnemonic: String) {
if (inputMnemonic.isEmpty() && uiState.importSeedPhraseState.error != null) {
val mediateState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = false)
updateUi {
uiBuilder.importSeedPhrase.updateError(mediateState, null)
}
return
}
interactor.validateMnemonicString(inputMnemonic)
.onSuccess { mnemonicComponents ->
importedMnemonicComponents = mnemonicComponents
val mediateState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = true)
updateUi { uiBuilder.importSeedPhrase.updateError(mediateState, null) }
}
.onFailure {
uiState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = false)
val error = it as? SeedPhraseError ?: return
importedMnemonicComponents = null
val mediateState = when (error) {
is SeedPhraseError.InvalidWords -> {
uiBuilder.importSeedPhrase.updateInvalidWords(uiState, error.words)
}
else -> uiState
}
updateUi { uiBuilder.importSeedPhrase.updateError(mediateState, error) }
}
}
private suspend fun updateSuggestions(fieldState: TextFieldState) {
val suggestions = interactor.getSuggestions(
text = fieldState.textFieldValue.text,
hasSelection = !fieldState.textFieldValue.selection.collapsed,
cursorPosition = fieldState.textFieldValue.selection.end,
)
updateUi { uiBuilder.importSeedPhrase.updateSuggestions(uiState, suggestions) }
}
// endregion ImportSeedPhrase
// region ButtonClickHandlers
private fun buttonCreateWalletClick() {
viewModelScope.launch(dispatchers.io) {
analyticsEventHandler.send(CreateWalletEvents.OnboardingSeedButtonCreateWallet)
mediator.createWallet(::handleWalletCreationResult)
}
}
private fun buttonImportWalletClick(
mnemonicComponents: List<String>?,
passphrase: String?,
seedPhraseSource: SeedPhraseSource,
) {
mnemonicComponents ?: return
viewModelScope.launch(dispatchers.io) {
mediator.importWallet(mnemonicComponents, passphrase, seedPhraseSource, ::handleWalletCreationResult)
}
}
private fun handleWalletCreationResult(result: CompletionResult<CreateWalletResponse>) {
when (result) {
is CompletionResult.Success -> {
isFinished = true
}
is CompletionResult.Failure -> {
// errors shows on the TangemSdk bottom sheet dialog
}
}
mediator.onWalletCreated(result)
}
private fun menuNavigateBackClick() {
router.navigateBack()
}
private fun menuChatClick() {
router.openChat()
}
private fun buttonOtherOptionsClick() {
analyticsEventHandler.send(CreateWalletEvents.OnboardingSeedButtonOtherOptions)
router.openScreen(SeedPhraseScreen.AboutSeedPhrase)
}
private fun buttonReadMoreAboutSeedPhraseClick() {
analyticsEventHandler.send(SeedPhraseEvents.ButtonReadMore)
val webUri = Uri.Builder()
.scheme("https")
.authority("tangem.com")
.appendPath(localeProvider.getWebUriLocaleLanguage())
.appendPath("blog/post/seed-phrase-a-risky-solution")
.build()
router.openUri(webUri)
}
private fun buttonGenerateSeedPhraseClick() {
val mnemonicTypes = listOf(MnemonicType.Mnemonic12, MnemonicType.Mnemonic24)
analyticsEventHandler.send(SeedPhraseEvents.ButtonGenerateSeedPhrase)
launchSingle {
updateUi { uiBuilder.generateMnemonicComponents(uiState) }
delay(DELAY_GENERATE_SEED_PHRASE)
interactor.generateMnemonics(mnemonicTypes)
.onSuccess { mnemonics ->
generatedMnemonicComponents = mnemonics.mapValues { it.value.mnemonicComponents }
val defaultMnemonic = mnemonics[selectedMnemonicType]?.mnemonicComponents ?: return@launchSingle
val mnemonicGridItems = toMnemonicGridList(defaultMnemonic)
updateUi {
router.openScreen(SeedPhraseScreen.YourSeedPhrase)
uiBuilder.mnemonicGenerated(uiState, mnemonicGridItems.toPersistentList())
}
}
.onFailure {
generatedMnemonicComponents = emptyMap()
// TODO: show error
}
}
}
private fun toMnemonicGridList(mnemonicComponents: List<String>): PersistentList<MnemonicGridItem> {
return mnemonicComponents.mapIndexed { index, word ->
MnemonicGridItem(
index = index.inc(),
mnemonic = word,
)
}.toPersistentList()
}
private fun buttonImportSeedPhraseClick() {
analyticsEventHandler.send(SeedPhraseEvents.ButtonImportWallet)
router.openScreen(SeedPhraseScreen.ImportSeedPhrase)
}
private fun buttonContinueClick() {
launchSingle {
updateUi {
uiBuilder.clearCheckSeedPhraseState(uiState)
}
router.openScreen(SeedPhraseScreen.CheckSeedPhrase)
}
}
private fun onSelectType(selectedSeedType: SegmentSeedType) {
launchSingle {
selectedMnemonicType = selectedSeedType.toMnemonicType()
val selectedMnemonic = generatedMnemonicComponents[selectedMnemonicType] ?: return@launchSingle
updateUi {
uiBuilder.selectSeedType(
uiState = uiState,
mnemonicGridItems = toMnemonicGridList(selectedMnemonic),
selectedSeedType = selectedSeedType,
)
}
}
}
private fun buttonSuggestedPhraseClick(suggestionIndex: Int) {
launchSingle {
val textFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue
val word = uiState.importSeedPhraseState.suggestionsList[suggestionIndex]
val cursorPosition = textFieldValue.selection.end
val insertResult = interactor.insertSuggestionWord(
text = textFieldValue.text,
suggestion = word,
cursorPosition = cursorPosition,
)
updateUi {
val mediateState = uiBuilder.importSeedPhrase.insertSuggestionWord(uiState, insertResult)
uiBuilder.importSeedPhrase.updateSuggestions(mediateState, persistentListOf())
// copy of TextFieldValue has no effect for textChange callback and seed validation doesn't work
// call validation force
}
validateMnemonic(uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue.text)
}
}
private fun buttonPassphraseInfoClick() {
launchSingle {
updateUi {
uiBuilder.importSeedPhrase.showPassphraseInfoBottomSheet(uiState) {
dismissPassphraseBottomSheet()
}
}
}
}
private fun dismissPassphraseBottomSheet() {
launchSingle {
updateUi { uiBuilder.importSeedPhrase.dismissPassphraseBottomSheet(uiState) }
}
}
// endregion ButtonClickHandlers
// region Utils
/**
* Updating the UI with a contract where all copying of objects are called in the Single thread context and
* updating the UiState in the main context.
*/
private suspend fun updateUi(updateBlock: suspend () -> OnboardingSeedPhraseState) {
withSingleContext {
val newState = updateBlock.invoke()
withMainContext { uiState = newState }
}
}
private fun createOrGetDebouncer(name: String): Debouncer {
return textFieldsDebouncers[name] ?: Debouncer().apply { textFieldsDebouncers[name] = this }
}
private fun SeedPhraseField.getState(uiState: OnboardingSeedPhraseState): TextFieldState = when (this) {
SeedPhraseField.Second -> uiState.checkSeedPhraseState.tvSecondPhrase
SeedPhraseField.Seventh -> uiState.checkSeedPhraseState.tvSeventhPhrase
SeedPhraseField.Eleventh -> uiState.checkSeedPhraseState.tvEleventhPhrase
}
private fun launchSingle(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(dispatchers.single, block = block)
}
private suspend fun <T> withMainContext(block: suspend CoroutineScope.() -> T): T {
return withContext(dispatchers.main, block)
}
private suspend fun <T> withSingleContext(block: suspend CoroutineScope.() -> T): T {
return withContext(dispatchers.single, block)
}
// endregion Utils
private companion object {
const val MNEMONIC_DEBOUNCER = "MnemonicDebouncer"
const val MNEMONIC_DEBOUNCE_DELAY = 700L
const val DELAY_GENERATE_SEED_PHRASE = 300L
}
}

View file

@ -19,8 +19,6 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
@ -34,7 +32,6 @@ import com.tangem.feature.walletsettings.entity.WalletSettingsUM
import com.tangem.feature.walletsettings.impl.R
import com.tangem.feature.walletsettings.utils.ItemsBuilder
import com.tangem.features.nft.NFTFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
@ -55,12 +52,10 @@ internal class WalletSettingsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsContextProxy: AnalyticsContextProxy,
private val reduxStateHolder: ReduxStateHolder,
private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val walletsRepository: WalletsRepository,
private val nftFeatureToggles: NFTFeatureToggles,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
) : Model() {
val params: WalletSettingsComponent.Params = paramsContainer.require()
@ -163,23 +158,12 @@ internal class WalletSettingsModel @Inject constructor(
analyticsEventHandler.send(Settings.ButtonCreateBackup)
analyticsContextProxy.addContext(scanResponse)
if (onboardingV2FeatureToggles.isOnboardingV2Enabled) {
router.push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.AddBackupWallet1,
),
)
} else {
reduxStateHolder.dispatch(
LegacyAction.StartOnboardingProcess(
scanResponse = scanResponse,
canSkipBackup = false,
),
)
router.push(AppRoute.OnboardingWallet())
}
router.push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.AddBackupWallet1,
),
)
}
private fun onCheckedNFTChange(isChecked: Boolean) {

View file

@ -96,9 +96,6 @@ dependencies {
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
//TODO: Create api/impl modules for onboarding [REDACTED_JIRA]
implementation(projects.features.onboarding)
/** Feature Apis */
implementation(projects.features.details.api)
implementation(projects.features.manageTokens.api)

Some files were not shown because too many files have changed in this diff Show more