Updated on 2026-08-14

This commit is contained in:
Tangem 2023-05-22 11:40:03 +03:00
commit ef1ed894bf
34 changed files with 771 additions and 175 deletions

View file

@ -25,6 +25,7 @@ import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPay
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AddMoreBackupCardsDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInProgressDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ResetBackupCardDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendBottomSheetDialog
@ -131,6 +132,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context)
is BackupDialog.ConfirmDiscardingBackup -> ConfirmDiscardingBackupDialog.create(context)
is BackupDialog.ResetBackupCard -> ResetBackupCardDialog.create(context)
is SaltPayDialog.Activation.NoGas -> NoFundsForActivationDialog.create(context)
is SaltPayDialog.Activation.PutVisaCard -> PutVisaCardDialog.create(context)
is SaltPayDialog.Activation.OnError -> RegistrationErrorDialog.create(context, state.dialog)

View file

@ -5,22 +5,14 @@ import androidx.annotation.StringRes
import com.tangem.Message
import com.tangem.TangemSdk
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CardFilter
import com.tangem.common.CompletionResult
import com.tangem.common.SuccessResponse
import com.tangem.common.UserCode
import com.tangem.common.UserCodeType
import com.tangem.common.*
import com.tangem.common.biometric.BiometricManager
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.Config
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.core.*
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.map
import com.tangem.common.usersCode.UserCodeRepository
import com.tangem.core.analytics.Analytics
import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
@ -45,6 +37,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
@Suppress("TooManyFunctions")
class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) {
private val userCodeRepository by lazy {
@ -91,6 +84,21 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
)
}
suspend fun importWallet(
scanResponse: ScanResponse,
mnemonic: String,
): CompletionResult<CreateProductWalletTaskResponse> {
return when (val seedResult = DefaultMnemonic(mnemonic, tangemSdk.wordlist).generateSeed()) {
is CompletionResult.Success -> runTaskAsync(
CreateProductWalletTask(scanResponse.cardTypesResolver, seedResult.data),
scanResponse.card.cardId,
Message(context.getString(R.string.initial_message_create_wallet_body)),
)
is CompletionResult.Failure -> CompletionResult.Failure(seedResult.error)
}
}
private fun sendScanResultsToAnalytics(result: CompletionResult<ScanResponse>) {
if (result is CompletionResult.Failure) {
(result.error as? TangemSdkError)?.let { error ->
@ -115,7 +123,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
}
suspend fun resetToFactorySettings(cardId: String): CompletionResult<CardDTO> {
suspend fun resetToFactorySettings(cardId: String? = null): CompletionResult<CardDTO> {
return runTaskAsyncReturnOnMain(
runnable = ResetToFactorySettingsTask(),
cardId = cardId,

View file

@ -56,6 +56,7 @@ private data class CreateWalletResponse(
class CreateProductWalletTask(
private val cardTypesResolver: CardTypesResolver,
private val seed: ByteArray? = null,
) : CardSessionRunnable<CreateProductWalletTaskResponse> {
override val allowsRequestAccessCodeFromRepository: Boolean = false
@ -74,7 +75,8 @@ class CreateProductWalletTask(
cardTypesResolver.isTangemNote() -> CreateWalletTangemNote(cardTypesResolver)
cardTypesResolver.isTangemTwins() ->
throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
else -> CreateWalletTangemWallet()
else -> CreateWalletTangemWallet(seed)
}
commandProcessor.proceed(cardDto, session) {
when (it) {
@ -129,7 +131,9 @@ private class CreateWalletTangemNote(private val cardTypesResolver: CardTypesRes
}
}
private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWalletTaskResponse> {
private class CreateWalletTangemWallet(
private val seed: ByteArray?,
) : ProductCommandProcessor<CreateProductWalletTaskResponse> {
private var primaryCard: PrimaryCard? = null
@ -149,7 +153,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
return
}
CreateWalletsTask(curves).run(session) { result ->
CreateWalletsTask(curves, seed).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
proceedWithCreatedWallets(

View file

@ -18,6 +18,7 @@ class CreateWalletsResponse(
class CreateWalletsTask(
private val curves: List<EllipticCurve>,
private val seed: ByteArray? = null,
) : CardSessionRunnable<CreateWalletsResponse> {
private val createdWalletsResponses = mutableListOf<CreateWalletResponse>()
@ -37,7 +38,7 @@ class CreateWalletsTask(
session: CardSession,
callback: (result: CompletionResult<CreateWalletsResponse>) -> Unit,
) {
CreateWalletTask(curve).run(session) { result ->
CreateWalletTask(curve, seed).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
createdWalletsResponses.add(result.data)
@ -47,6 +48,7 @@ class CreateWalletsTask(
}
createWallet(curves[createdWalletsResponses.size], session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}

View file

@ -40,6 +40,14 @@ object OnboardingHelper {
cardInfoStorage.isActivationInProgress(cardId)
}
}
response.cardTypesResolver.isWallet2() -> {
val emptyWallets = response.card.wallets.isEmpty()
val activationInProgress = cardInfoStorage.isActivationInProgress(cardId)
val backupNotActive = response.card.backupStatus?.isActive != true
emptyWallets || activationInProgress || backupNotActive
}
response.card.wallets.isNotEmpty() -> cardInfoStorage.isActivationInProgress(cardId)
else -> true
}

View file

@ -1,13 +1,21 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.net.Uri
import com.tangem.common.CompletionResult
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
object Init : OnboardingWalletAction()
object GetToCreateWalletStep : OnboardingWalletAction()
object GetToSaltPayStep : OnboardingWalletAction()
object CreateWallet : OnboardingWalletAction()
data class WalletWasCreated(
val result: CompletionResult<CreateProductWalletTaskResponse>,
) : OnboardingWalletAction()
object Done : OnboardingWalletAction()
object FinishOnboarding : OnboardingWalletAction()
@ -19,6 +27,24 @@ sealed class OnboardingWalletAction : Action {
object OnBackPressed : 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 callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) : OnboardingWallet2Action()
data class WalletWasCreated(
val result: CompletionResult<CreateWalletResponse>,
) : OnboardingWallet2Action()
}
sealed class BackupAction : Action {
object IntroduceBackup : BackupAction()
@ -62,4 +88,6 @@ sealed class BackupAction : Action {
object DiscardBackup : BackupAction()
object DiscardSavedBackup : BackupAction()
object ResumeFoundUnfinishedBackup : BackupAction()
object ResetBackupCard : BackupAction()
}

View file

@ -3,15 +3,17 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import android.net.Uri
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ifNotNull
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.operations.backup.BackupService
import com.tangem.tap.backupService
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
@ -20,6 +22,7 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
@ -29,12 +32,6 @@ import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivat
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
@ -47,7 +44,10 @@ object OnboardingWalletMiddleware {
private val onboardingWalletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleWalletAction(action, state, dispatch)
when (action) {
is OnboardingWallet2Action -> handleWallet2Action(action, state)
else -> handleWalletAction(action, state, dispatch)
}
next(action)
}
}
@ -114,42 +114,44 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
}
}
is OnboardingWalletAction.CreateWallet -> {
scanResponse ?: return
scope.launch {
scanResponse ?: return@launch
val result = tangemSdkManager.createProductWallet(scanResponse)
withMainContext {
when (result) {
is CompletionResult.Success -> {
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.state.globalState.topUpController?.registerEmptyWallet(updatedResponse)
store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(result))
}
}
is OnboardingWalletAction.WalletWasCreated -> {
scanResponse ?: return
when (val result = action.result) {
is CompletionResult.Success -> {
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.state.globalState.topUpController?.registerEmptyWallet(updatedResponse)
val blockchainNetworks = if (DemoHelper.isDemoCardId(result.data.card.cardId)) {
DemoHelper.config.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}.map { blockchain ->
BlockchainNetwork(blockchain, result.data.card)
}
scope.launch {
userTokensRepository.saveUserTokens(
card = result.data.card,
tokens = blockchainNetworks.toCurrencies(),
)
}
startCardActivation(updatedResponse)
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
is CompletionResult.Failure -> Unit
val blockchainNetworks = if (DemoHelper.isDemoCardId(result.data.card.cardId)) {
DemoHelper.config.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}.map { blockchain ->
BlockchainNetwork(blockchain, result.data.card)
}
scope.launch {
userTokensRepository.saveUserTokens(
card = result.data.card,
tokens = blockchainNetworks.toCurrencies(),
)
}
startCardActivation(updatedResponse)
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
is CompletionResult.Failure -> Unit
}
}
OnboardingWalletAction.FinishOnboarding -> {
@ -194,6 +196,93 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleWallet2Action(action: OnboardingWallet2Action, state: () -> AppState?) {
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)) {
is CompletionResult.Success -> {
val response = CreateWalletResponse(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
)
CompletionResult.Success(response)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
withMainContext {
action.callback(mediateResult)
}
}
}
is OnboardingWallet2Action.ImportWallet -> {
scanResponse ?: return
scope.launch {
val mediateResult = when (
val result = tangemSdkManager.importWallet(
scanResponse = scanResponse,
mnemonic = action.mnemonicComponents.joinToString(" "),
)
) {
is CompletionResult.Success -> {
val response = CreateWalletResponse(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
)
CompletionResult.Success(response)
}
is CompletionResult.Failure -> {
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)
}
}
store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(result))
}
else -> Unit
}
}
private fun updateScanResponseAfterBackup(scanResponse: ScanResponse, backupState: BackupState): ScanResponse {
val card = if (backupState.backupCardsNumber > 0) {
val cardsCount = backupState.backupCardsNumber
@ -263,7 +352,18 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.AddBackupCard.Success)
}
is CompletionResult.Failure -> Unit
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.BackupFailedNotEmptyWallets &&
onboardingWalletState.wallet2State != null
) {
store.dispatchOnMain(
GlobalAction.ShowDialog(
BackupDialog.ResetBackupCard,
),
)
}
}
}
}
}
@ -368,6 +468,11 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
Analytics.send(Onboarding.Finished())
finishCardActivation(notActivatedCardIds)
}
is BackupAction.ResetBackupCard -> {
scope.launch { tangemSdkManager.resetToFactorySettings() }
}
else -> Unit
}
}

View file

@ -19,8 +19,8 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
is GlobalAction.Onboarding -> ReducerForGlobalAction.reduce(action, state)
is OnboardingSaltPayAction -> OnboardingSaltPayReducer.reduce(action, state)
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState, state.isSaltPay))
is OnboardingWalletAction.GetToCreateWalletStep -> OnboardingWalletState(
is OnboardingWallet2Action -> OnboardingWallet2Reducer.reduce(action, state)
is OnboardingWalletAction.GetToCreateWalletStep -> state.copy(
step = OnboardingWalletStep.CreateWallet,
)
is OnboardingWalletAction.GetToSaltPayStep -> state.copy(
@ -65,6 +65,17 @@ private object ReducerForGlobalAction {
private fun maxBackupCards(isSaltPay: Boolean): Int = if (isSaltPay) 1 else 2
}
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")
fun reduce(action: BackupAction, state: BackupState, isSaltPay: Boolean): BackupState {

View file

@ -14,6 +14,7 @@ import org.rekotlin.StateType
*/
data class OnboardingWalletState(
val step: OnboardingWalletStep = OnboardingWalletStep.None,
val wallet2State: OnboardingWallet2State? = null,
val backupState: BackupState = BackupState(),
val onboardingSaltPayState: OnboardingSaltPayState? = null,
val isSaltPay: Boolean = false,
@ -31,14 +32,14 @@ data class OnboardingWalletState(
}
@Suppress("MagicNumber")
fun getMaxProgress(): Int = when {
isSaltPay -> 12
else -> 6
fun getMaxProgress(): Int {
val baseProgress = if (isSaltPay) 12 else 6
return getWallet2Progress() + baseProgress
}
@Suppress("ComplexMethod", "MagicNumber")
fun getProgressStep(): Int {
return when {
val progressByStep = when {
step == OnboardingWalletStep.CreateWallet -> 1
step == OnboardingWalletStep.Backup -> {
when (backupState.backupStep) {
@ -69,11 +70,19 @@ data class OnboardingWalletState(
step == OnboardingWalletStep.Done -> getMaxProgress()
else -> 1
}
return getWallet2Progress() + progressByStep
}
private fun getWallet2Progress(): Int = wallet2State?.maxProgress ?: 0
}
data class OnboardingWallet2State(
val maxProgress: Int,
)
enum class OnboardingWalletStep {
None, CreateWallet, Backup, SaltPay, Done
None, CreateWallet, SaltPay, Backup, Done
}
data class BackupState(
@ -113,4 +122,5 @@ sealed class BackupDialog : StateDialog {
object BackupInProgress : BackupDialog()
object UnfinishedBackupFound : BackupDialog()
object ConfirmDiscardingBackup : BackupDialog()
object ResetBackupCard : BackupDialog()
}

View file

@ -18,15 +18,7 @@ import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.compose.PinCodeWidget
import com.tangem.tap.common.extensions.configureSettings
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.setDrawable
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.stop
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
@ -34,11 +26,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.saltPay.UtorgWebChromeClient
import com.tangem.tap.features.onboarding.products.wallet.saltPay.UtorgWebViewClient
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
@ -56,7 +44,7 @@ import com.tangem.wallet.databinding.LayoutOnboardingMainBinding
/**
[REDACTED_AUTHOR]
*/
internal class OnboardingSaltPayView(
internal class OnboardingSaltPayStateHandler(
private val walletFragment: OnboardingWalletFragment,
) {

View file

@ -0,0 +1,54 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import androidx.compose.runtime.collectAsState
import com.tangem.feature.onboarding.api.OnboardingSeedPhrase
import com.tangem.feature.onboarding.api.OnboardingSeedPhraseApi
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
/**
[REDACTED_AUTHOR]
*/
internal class OnboardingSeedPhraseStateHandler(
private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhrase(),
) {
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 {
onboardingSeedPhraseApi.ScreenContent(
uiState = viewModel.uiState,
subScreen = viewModel.currentScreen.collectAsState().value,
progress = viewModel.progress.collectAsState(0).value.toFloat() / onboardingWalletMaxProgress,
)
}
}
}

View file

@ -10,6 +10,7 @@ import android.widget.FrameLayout
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
import androidx.fragment.app.viewModels
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import by.kirich1409.viewbindingdelegate.viewBinding
@ -17,46 +18,55 @@ import coil.load
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.core.analytics.Analytics
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
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.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.hide
import com.tangem.tap.common.extensions.inflate
import com.tangem.tap.common.extensions.resourceUri
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
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.BackupAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.ui.OnboardingSaltPayView
import com.tangem.tap.features.onboarding.products.wallet.saltPay.ui.OnboardingSaltPayStateHandler
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
import com.tangem.wallet.databinding.LayoutOnboardingSaltpayBinding
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
@Suppress("LargeClass", "MagicNumber")
@AndroidEntryPoint
class OnboardingWalletFragment :
BaseFragment(R.layout.fragment_onboarding_wallet),
StoreSubscriber<OnboardingWalletState>,
FragmentOnBackPressedHandler {
private val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
internal val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind)
internal val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
internal val bindingSeedPhrase: LayoutOnboardingSeedPhraseBinding by lazy { binding.onboardingSeedPhraseContainer }
internal val bindingSaltPay: LayoutOnboardingSaltpayBinding by lazy { binding.onboardingSaltpayContainer }
private val onboardingSaltPayView: OnboardingSaltPayView = OnboardingSaltPayView(this)
private val seedPhraseStateHandler: OnboardingSeedPhraseStateHandler = OnboardingSeedPhraseStateHandler()
private val seedPhraseViewModel by viewModels<SeedPhraseViewModel>()
private val saltPayStateHandler: OnboardingSaltPayStateHandler = OnboardingSaltPayStateHandler(this)
private lateinit var cardsWidget: WalletCardsWidget
private var accessCodeDialog: AccessCodeDialog? = null
@ -69,6 +79,13 @@ class OnboardingWalletFragment :
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
seedPhraseViewModel.setRouter(makeSeedPhraseRouter())
seedPhraseViewModel.setMediator(makeSeedPhraseMediator())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -88,6 +105,7 @@ class OnboardingWalletFragment :
binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
addBackPressHandler(this)
store.dispatch(OnboardingWallet2Action.Init(seedPhraseViewModel.maxProgress))
store.dispatch(OnboardingWalletAction.Init)
store.dispatch(OnboardingSaltPayAction.Init)
store.dispatch(
@ -97,7 +115,7 @@ class OnboardingWalletFragment :
)
}
override fun loadToolbarMenu(): MenuProvider? = OnboardingMenuProvider()
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider()
private fun reInitCardsWidgetIfNeeded(backupCardsCounts: Int) = with(binding) {
val viewBackupCount = flCardsContainer.childCount - 1
@ -159,13 +177,19 @@ class OnboardingWalletFragment :
pbBinding.pbState.max = state.getMaxProgress()
pbBinding.pbState.progress = state.getProgressStep()
if (state.isSaltPay) {
onboardingSaltPayView.newState(state)
} else {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
loadImageIntoImageView(state.cardArtworkUri, binding.imvFirstBackupCard)
loadImageIntoImageView(state.cardArtworkUri, binding.imvSecondBackupCard)
handleOnboardingStep(state)
when {
state.wallet2State != null -> {
seedPhraseStateHandler.newState(this, state, seedPhraseViewModel)
}
state.isSaltPay -> {
saltPayStateHandler.newState(state)
}
else -> {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
loadImageIntoImageView(state.cardArtworkUri, binding.imvFirstBackupCard)
loadImageIntoImageView(state.cardArtworkUri, binding.imvSecondBackupCard)
handleOnboardingStep(state)
}
}
}
@ -180,7 +204,12 @@ class OnboardingWalletFragment :
internal fun handleOnboardingStep(state: OnboardingWalletState) {
when (state.step) {
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
OnboardingWalletStep.Backup -> setBackupState(state.backupState, state.isSaltPay)
OnboardingWalletStep.Backup -> setBackupState(
state = state.backupState,
isSaltPay = state.isSaltPay,
isWallet2 = state.wallet2State != null,
)
else -> {}
}
}
@ -201,9 +230,9 @@ class OnboardingWalletFragment :
animator.setupCreateWalletState()
}
private fun setBackupState(state: BackupState, isSaltPay: Boolean) {
private fun setBackupState(state: BackupState, isSaltPay: Boolean, isWallet2: Boolean) {
when (state.backupStep) {
BackupStep.InitBackup -> showBackupIntro(state, isSaltPay)
BackupStep.InitBackup -> showBackupIntro(state, isSaltPay, isWallet2)
BackupStep.ScanOriginCard -> showScanOriginCard()
BackupStep.AddBackupCards -> showAddBackupCards(state, isSaltPay)
BackupStep.SetAccessCode -> showSetAccessCode()
@ -215,7 +244,7 @@ class OnboardingWalletFragment :
}
}
private fun showBackupIntro(state: BackupState, isSaltPay: Boolean) = with(binding) {
private fun showBackupIntro(state: BackupState, isSaltPay: Boolean, isWallet2: Boolean) = with(binding) {
imvFirstBackupCard.show()
imvSecondBackupCard.show()
@ -230,7 +259,7 @@ class OnboardingWalletFragment :
btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
btnWalletAlternativeAction.setOnClickListener { store.dispatch(BackupAction.SkipBackup) }
btnWalletAlternativeAction.show(state.canSkipBackup && !isSaltPay)
btnWalletAlternativeAction.show(state.canSkipBackup && !isSaltPay && !isWallet2)
}
animator.showBackupIntro(state)
}
@ -271,7 +300,7 @@ class OnboardingWalletFragment :
layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0
if (isSaltPay) {
onboardingSaltPayView.showAddBackupCards(state)
saltPayStateHandler.showAddBackupCards(state)
} else {
when (state.backupCardsNumber) {
0 -> {
@ -443,4 +472,46 @@ class OnboardingWalletFragment :
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
return typedValue.float
}
private fun makeSeedPhraseRouter(): SeedPhraseRouter = SeedPhraseRouter(
onBack = ::handleOnBackPressed,
onOpenChat = {
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
},
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>,
callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) {
store.dispatch(OnboardingWallet2Action.ImportWallet(mnemonicComponents, callback))
}
override fun allowScreenshots(allow: Boolean) {
mainScope.launch {
if (allow) {
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
requireActivity().window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
}
}
}
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
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): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.common_warning)
setMessage(R.string.onboarding_linking_error_card_with_wallets)
setPositiveButton(R.string.common_continue) { _, _ ->
store.dispatch(BackupAction.ResetBackupCard)
}
setNegativeButton(R.string.common_cancel) { _, _ ->
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -228,6 +228,13 @@
android:layout_height="match_parent"
android:visibility="gone" />
<include
android:id="@+id/onboarding_seed_phrase_container"
layout="@layout/layout_onboarding_seed_phrase"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.appcompat.widget.LinearLayoutCompat>
<include

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.compose.ui.platform.ComposeView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/onboarding_seed_phrase_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View file

@ -7,6 +7,6 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:progressBackgroundTint="#401ACE80"
android:progressTint="@color/accent" />
android:progressBackgroundTint="#32000000"
android:progressTint="@color/background_action" />
</merge>

View file

@ -45,12 +45,11 @@
<color name="all_colors_azure">#007AFF</color>
<color name="background_action">#000000</color>
<color name="background_primary">#FFFFFF</color>
<color name="background_secondary">#F5F5F5</color>
<color name="button_disabled">#EBEBEB</color>
<color name="button_primary">#1E1E1E</color>
<color name="button_secondary">#EBEBEB</color>

View file

@ -184,6 +184,7 @@
<string name="onboarding_exit_alert_message">В этом случае вам будет необходимо начать процесс заново.</string>
<string name="onboarding_exit_alert_title">Вы хотите выйти из процесса активации?</string>
<string name="onboarding_getting_started">Подготовка</string>
<string name="onboarding_linking_error_card_with_wallets">Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа?</string>
<string name="onboarding_navbar_kyc_progress">Подтвердите свою личность</string>
<string name="onboarding_navbar_kyc_start">Верификация клиента</string>
<string name="onboarding_navbar_pin">Код доступа</string>

View file

@ -182,6 +182,7 @@
<string name="onboarding_exit_alert_message">In this case, you will need to start from the beginning.</string>
<string name="onboarding_exit_alert_title">Do you want to exit the activation process?</string>
<string name="onboarding_getting_started">Getting started</string>
<string name="onboarding_linking_error_card_with_wallets">Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet?</string>
<string name="onboarding_navbar_kyc_progress">Verify your identity</string>
<string name="onboarding_navbar_kyc_start">KYC</string>
<string name="onboarding_navbar_pin">Pin code</string>

View file

@ -26,7 +26,9 @@ class TangemCardTypesResolver(
card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable &&
!card.isSaltPay
override fun isWallet2(): Boolean = card.firmwareVersion >= FirmwareVersion.KeysImportAvailable
override fun isWallet2(): Boolean =
card.firmwareVersion >= FirmwareVersion.KeysImportAvailable && card.settings.isKeysImportAllowed
override fun isSaltPay(): Boolean = productType == ProductType.SaltPay
override fun isSaltPayVisa(): Boolean = card.isSaltPayVisa
override fun isSaltPayWallet(): Boolean = card.isSaltPayWallet

View file

@ -8,7 +8,7 @@ import com.tangem.common.card.EncryptionMode
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.operations.attestation.Attestation
import java.util.Date
import java.util.*
import com.tangem.common.card.FirmwareVersion as SdkFirmwareVersion
/**
@ -103,6 +103,7 @@ data class CardDTO(
val supportedEncryptionModes: List<EncryptionMode>,
val isFilesAllowed: Boolean,
val isHDWalletAllowed: Boolean,
val isKeysImportAllowed: Boolean,
) {
constructor(settings: Card.Settings) : this(
securityDelay = settings.securityDelay,
@ -115,6 +116,7 @@ data class CardDTO(
supportedEncryptionModes = settings.supportedEncryptionModes,
isFilesAllowed = settings.isFilesAllowed,
isHDWalletAllowed = settings.isHDWalletAllowed,
isKeysImportAllowed = settings.isKeysImportAllowed,
)
}

View file

@ -10,6 +10,7 @@ plugins {
dependencies {
/** Core modules */
implementation(project(":common"))
implementation(project(":domain:models"))
implementation(project(":core:featuretoggles"))
implementation(project(":core:datasource"))
implementation(project(":core:analytics"))
@ -17,6 +18,12 @@ dependencies {
implementation(project(":core:ui"))
implementation(project(":core:res"))
/** Tangem libraries */
implementation(deps.tangem.card.core)
implementation(deps.tangem.card.android) {
exclude(module = "joda-time")
}
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
implementation(deps.androidx.appCompat)

View file

@ -0,0 +1,79 @@
package com.tangem.feature.onboarding.api
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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 OnboardingSeedPhrase : OnboardingSeedPhraseApi {
@Composable
override fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float) {
BackHandler(onBack = uiState.onBackClick)
TangemTheme {
Column {
ProgressIndicator(progress)
Content(subScreen, uiState)
}
}
}
}
@Composable
private fun ProgressIndicator(progress: Float) {
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = TangemTheme.dimens.size8),
) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.size16),
color = TangemTheme.colors.background.action,
backgroundColor = TangemTheme.colors.background.action.copy(alpha = 0.081f),
progress = progress,
)
}
}
@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

@ -0,0 +1,14 @@
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,21 +1,17 @@
package com.tangem.feature.onboarding.data
import com.tangem.crypto.bip39.*
import com.tangem.datasource.asset.AssetReader
import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.bip39.EntropyLength
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.Wordlist
/**
[REDACTED_AUTHOR]
*/
internal class DefaultMnemonicRepository(
private val assetReader: AssetReader,
private val bip39Wordlist: Wordlist,
) : MnemonicRepository {
// TODO: change initialization after adding it to the sdk
private val bip39Wordlist: Wordlist by lazy {
assetReader.openFile(MNEMONIC_FILE_NAME)
.use { BIP39Wordlist(it) }
}
private val bip39Words: Set<String> by lazy { bip39Wordlist.words.toHashSet() }
override fun getWordsDictionary(): Set<String> = bip39Words
@ -25,8 +21,4 @@ internal class DefaultMnemonicRepository(
}
override fun createMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(mnemonicString, bip39Wordlist)
companion object {
private const val MNEMONIC_FILE_NAME = "mnemonic/mnemonic_dictionary_en.txt"
}
}

View file

@ -1,11 +1,14 @@
package com.tangem.feature.onboarding.data.di
import com.tangem.datasource.asset.AssetReader
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
@ -15,7 +18,7 @@ class OnboardingDataModule {
@Provides
@Singleton
fun provideSeedPhraseSdkRepository(assetReader: AssetReader): MnemonicRepository {
return DefaultMnemonicRepository(assetReader)
fun provideSeedPhraseSdkRepository(@ApplicationContext context: Context): MnemonicRepository {
return DefaultMnemonicRepository(Wordlist.getWordlist(context))
}
}

View file

@ -0,0 +1,15 @@
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

@ -9,20 +9,15 @@ import kotlinx.collections.immutable.persistentListOf
[REDACTED_AUTHOR]
*/
data class OnboardingSeedPhraseState(
val step: OnboardingSeedPhraseStep,
val introState: IntroState,
val aboutState: AboutState,
val yourSeedPhraseState: YourSeedPhraseState,
val checkSeedPhraseState: CheckSeedPhraseState,
val importSeedPhraseState: ImportSeedPhraseState,
val menuButtonChat: ButtonState,
val isOnboardingFinished: Boolean = false,
val onBackClick: () -> Unit,
)
enum class OnboardingSeedPhraseStep {
Intro, AboutSeedPhrase, YourSeedPhrase, CheckSeedPhrase, ImportSeedPhrase
}
data class IntroState(
val cardImageUrl: String? = null,
val buttonCreateWallet: ButtonState,

View file

@ -12,6 +12,7 @@ data class UiActions(
val checkSeedPhraseActions: CheckSeedPhraseUiAction,
val importSeedPhraseActions: ImportSeedPhraseUiAction,
val menuChatClick: () -> Unit,
val menuNavigateBackClick: () -> Unit,
)
data class IntroUiAction(

View file

@ -93,7 +93,6 @@ private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modif
),
colors = TangemTextFieldsDefault.defaultTextFieldColors,
)
Box(
modifier = Modifier

View file

@ -14,7 +14,6 @@ class StateBuilder(
val importSeedPhrase: ImportSeedPhraseStateBuilder = ImportSeedPhraseStateBuilder()
fun init(): OnboardingSeedPhraseState = OnboardingSeedPhraseState(
step = OnboardingSeedPhraseStep.Intro,
introState = IntroState(
buttonCreateWallet = ButtonState(
onClick = uiActions.introActions.buttonCreateWalletClick,
@ -77,13 +76,9 @@ class StateBuilder(
menuButtonChat = ButtonState(
onClick = uiActions.menuChatClick,
),
onBackClick = uiActions.menuNavigateBackClick,
)
fun changeStep(uiState: OnboardingSeedPhraseState, step: OnboardingSeedPhraseStep): OnboardingSeedPhraseState =
uiState.copy(
step = step,
)
fun generateMnemonicComponents(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState {
return uiState.copy(
aboutState = uiState.aboutState.copy(
@ -103,7 +98,6 @@ class StateBuilder(
): OnboardingSeedPhraseState {
return updateMnemonicComponents(uiState, mnemonicGridItems)
.copy(
step = OnboardingSeedPhraseStep.YourSeedPhrase,
aboutState = uiState.aboutState.copy(
buttonGenerateSeedPhrase = uiState.aboutState.buttonGenerateSeedPhrase.copy(
showProgress = false,

View file

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

View file

@ -0,0 +1,56 @@
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 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
var currentScreenState by mutableStateOf(SeedPhraseScreen.Intro)
private set
fun navigateBack() {
_currentScreen.value = when (_currentScreen.value) {
SeedPhraseScreen.Intro -> {
onBack.invoke()
return
}
SeedPhraseScreen.AboutSeedPhrase -> SeedPhraseScreen.Intro
SeedPhraseScreen.YourSeedPhrase -> SeedPhraseScreen.AboutSeedPhrase
SeedPhraseScreen.CheckSeedPhrase -> SeedPhraseScreen.YourSeedPhrase
SeedPhraseScreen.ImportSeedPhrase -> SeedPhraseScreen.AboutSeedPhrase
}
}
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,14 +1,27 @@
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.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.domain.SeedPhraseError
import com.tangem.feature.onboarding.domain.SeedPhraseInteractor
import com.tangem.feature.onboarding.presentation.wallet2.model.*
import com.tangem.feature.onboarding.presentation.wallet2.model.AboutUiAction
import com.tangem.feature.onboarding.presentation.wallet2.model.CheckSeedPhraseUiAction
import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseUiAction
import com.tangem.feature.onboarding.presentation.wallet2.model.IntroUiAction
import com.tangem.feature.onboarding.presentation.wallet2.model.MnemonicGridItem
import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField
import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState
import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldUiAction
import com.tangem.feature.onboarding.presentation.wallet2.model.UiActions
import com.tangem.feature.onboarding.presentation.wallet2.model.YourSeedPhraseUiAction
import com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders.StateBuilder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.Debouncer
@ -16,7 +29,16 @@ import com.tangem.utils.extensions.isEven
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@ -34,18 +56,48 @@ class SeedPhraseViewModel @Inject constructor(
var uiState: OnboardingSeedPhraseState by mutableStateOf(uiBuilder.init())
private set
val currentStep: OnboardingSeedPhraseStep
get() = uiState.step
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 suggestionWordInserted: AtomicBoolean = AtomicBoolean(false)
private var generatedMnemonicComponents: List<String>? = null
private var importedMnemonicComponents: List<String>? = null
override fun onCleared() {
textFieldsDebouncers.forEach { entry -> entry.value.release() }
textFieldsDebouncers.clear()
super.onCleared()
}
fun setRouter(router: SeedPhraseRouter) {
this.router = router
}
fun setMediator(mediator: SeedPhraseMediator) {
this.mediator = mediator
}
private fun createUiActions(): UiActions = UiActions(
introActions = IntroUiAction(
buttonCreateWalletClick = ::buttonCreateWalletClick,
@ -60,7 +112,7 @@ class SeedPhraseViewModel @Inject constructor(
buttonContinueClick = ::buttonContinueClick,
),
checkSeedPhraseActions = CheckSeedPhraseUiAction(
buttonCreateWalletClick = ::buttonCreateWalletWithSeedPhraseClick,
buttonCreateWalletClick = { buttonImportWalletClick(generatedMnemonicComponents) },
secondTextFieldAction = TextFieldUiAction(
onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Second, value) },
),
@ -76,9 +128,10 @@ class SeedPhraseViewModel @Inject constructor(
onTextFieldChanged = { value -> onSeedPhraseTextFieldChanged(value) },
),
suggestedPhraseClick = ::buttonSuggestedPhraseClick,
buttonCreateWalletClick = ::buttonCreateWalletWithSeedPhraseClick,
buttonCreateWalletClick = { buttonImportWalletClick(importedMnemonicComponents) },
),
menuChatClick = ::menuChatClick,
menuNavigateBackClick = ::menuNavigateBackClick,
)
// region CheckSeedPhrase
@ -116,8 +169,7 @@ class SeedPhraseViewModel @Inject constructor(
val isSameText = textFieldValue.text == oldTextFieldValue.text
val isCursorMoved = textFieldValue.selection != oldTextFieldValue.selection
val mediateState = uiBuilder.importSeedPhrase.updateTextField(uiState, textFieldValue)
uiState = uiBuilder.importSeedPhrase.updateCreateWalletButton(mediateState, enabled = false)
uiState = uiBuilder.importSeedPhrase.updateTextField(uiState, textFieldValue)
val fieldState = uiState.importSeedPhraseState.tvSeedPhrase
val inputMnemonic = fieldState.textFieldValue.text
@ -148,21 +200,29 @@ class SeedPhraseViewModel @Inject constructor(
private suspend fun validateMnemonic(inputMnemonic: String) {
if (inputMnemonic.isEmpty() && uiState.importSeedPhraseState.error != null) {
updateUi { uiBuilder.importSeedPhrase.updateError(uiState, null) }
val mediateState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = false)
updateUi {
uiBuilder.importSeedPhrase.updateError(mediateState, null)
}
return
}
interactor.validateMnemonicString(inputMnemonic)
.onSuccess {
updateUi { uiBuilder.importSeedPhrase.updateError(uiState, null) }
.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) }
@ -180,22 +240,46 @@ class SeedPhraseViewModel @Inject constructor(
// endregion ImportSeedPhrase
// region ButtonClickHandlers
@Suppress("EmptyFunctionBlock")
private fun buttonCreateWalletClick() {
}
@Suppress("EmptyFunctionBlock")
private fun buttonCreateWalletWithSeedPhraseClick() {
}
private fun buttonOtherOptionsClick() {
viewModelScope.launchSingle {
updateUi { uiBuilder.changeStep(uiState, OnboardingSeedPhraseStep.AboutSeedPhrase) }
viewModelScope.launch(dispatchers.io) {
mediator.createWallet(::handleWalletCreationResult)
}
}
private fun buttonImportWalletClick(mnemonicComponents: List<String>?) {
mnemonicComponents ?: return
viewModelScope.launch(dispatchers.io) {
mediator.importWallet(mnemonicComponents, ::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() {
router.openScreen(SeedPhraseScreen.AboutSeedPhrase)
}
private fun buttonReadMoreAboutSeedPhraseClick() {
// TODO: open the about info through a router
router.openUri(URI_ABOUT_SEED_PHRASE)
}
private fun buttonGenerateSeedPhraseClick() {
@ -204,15 +288,28 @@ class SeedPhraseViewModel @Inject constructor(
delay(DELAY_GENERATE_SEED_PHRASE)
interactor.generateMnemonic()
.onSuccess { mnemonic ->
generatedMnemonicComponents = mnemonic.mnemonicComponents
val mnemonicGridItems = generateMnemonicGridList(mnemonic.mnemonicComponents)
updateUi { uiBuilder.mnemonicGenerated(uiState, mnemonicGridItems.toImmutableList()) }
setScreenshotsEnabled()
updateUi {
router.openScreen(SeedPhraseScreen.YourSeedPhrase)
uiBuilder.mnemonicGenerated(uiState, mnemonicGridItems.toImmutableList())
}
}
.onFailure {
generatedMnemonicComponents = null
// TODO: show error
}
}
}
private fun setScreenshotsEnabled() {
router.currentScreen.onEach {
val allowScreenshots = it != SeedPhraseScreen.YourSeedPhrase
mediator.allowScreenshots(allowScreenshots)
}.launchIn(viewModelScope)
}
private suspend fun generateMnemonicGridList(mnemonicComponents: List<String>): ImmutableList<MnemonicGridItem> {
val size = mnemonicComponents.size
val splitIndex = if (size.isEven()) size / 2 else size / 2 + 1
@ -240,15 +337,12 @@ class SeedPhraseViewModel @Inject constructor(
}
private fun buttonImportSeedPhraseClick() {
viewModelScope.launchSingle {
updateUi { uiBuilder.changeStep(uiState, OnboardingSeedPhraseStep.ImportSeedPhrase) }
}
router.openScreen(SeedPhraseScreen.ImportSeedPhrase)
}
private fun buttonContinueClick() {
viewModelScope.launchSingle {
updateUi { uiBuilder.changeStep(uiState, OnboardingSeedPhraseStep.CheckSeedPhrase) }
}
router.openScreen(SeedPhraseScreen.CheckSeedPhrase)
mediator.allowScreenshots(true)
}
private fun buttonSuggestedPhraseClick(suggestionIndex: Int) {
@ -269,10 +363,6 @@ class SeedPhraseViewModel @Inject constructor(
}
}
}
private fun menuChatClick() {
// TODO: open the support chat through a router
}
// endregion ButtonClickHandlers
// region Utils
@ -311,6 +401,8 @@ class SeedPhraseViewModel @Inject constructor(
// endregion Utils
companion object {
private val URI_ABOUT_SEED_PHRASE =
Uri.parse("https://tangem.com/ru/blog/post/seed-phrase-a-risky-solution/")
private const val MNEMONIC_DEBOUNCER = "MnemonicDebouncer"
private const val MNEMONIC_DEBOUNCE_DELAY = 700L
private const val DELAY_GENERATE_SEED_PHRASE = 300L