Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-02 10:12:14 +03:00
parent 9b6de19ebe
commit ce602107bb
16 changed files with 178 additions and 115 deletions

View file

@ -55,6 +55,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
)
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(
titleRes = R.string.wallet_connect_title,

View file

@ -46,6 +46,8 @@ sealed class GlobalAction : Action {
data class StartForUnfinishedBackup(val addedBackupCardsCount: Int) : Onboarding()
object Stop : Onboarding()
data class ShouldResetCardOnCreate(val shouldReset: Boolean) : Onboarding()
}
object ScanFailsCounter {

View file

@ -30,6 +30,11 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
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

@ -36,4 +36,5 @@ typealias CryptoCurrencyName = String
data class OnboardingState(
val onboardingStarted: Boolean = false,
val onboardingManager: OnboardingManager? = null,
val shouldResetOnCreate: Boolean = false,
)

View file

@ -85,11 +85,15 @@ class TangemSdkManager(
).also { sendScanResultsToAnalytics(it) }
}
suspend fun createProductWallet(scanResponse: ScanResponse): CompletionResult<CreateProductWalletTaskResponse> {
suspend fun createProductWallet(
scanResponse: ScanResponse,
shouldReset: Boolean = false,
): CompletionResult<CreateProductWalletTaskResponse> {
return runTaskAsync(
runnable = CreateProductWalletTask(
cardTypesResolver = scanResponse.cardTypesResolver,
derivationStyleProvider = scanResponse.derivationStyleProvider,
shouldReset = shouldReset,
),
cardId = scanResponse.card.cardId,
initialMessage = Message(resources.getString(R.string.initial_message_create_wallet_body)),
@ -100,6 +104,7 @@ class TangemSdkManager(
suspend fun importWallet(
scanResponse: ScanResponse,
mnemonic: String,
shouldReset: Boolean,
): CompletionResult<CreateProductWalletTaskResponse> {
val defaultMnemonic = try {
DefaultMnemonic(mnemonic, tangemSdk.wordlist)
@ -108,9 +113,10 @@ class TangemSdkManager(
}
return runTaskAsync(
CreateProductWalletTask(
scanResponse.cardTypesResolver,
cardTypesResolver = scanResponse.cardTypesResolver,
derivationStyleProvider = scanResponse.derivationStyleProvider,
defaultMnemonic,
mnemonic = defaultMnemonic,
shouldReset = shouldReset,
),
scanResponse.card.cardId,
Message(resources.getString(R.string.initial_message_create_wallet_body)),

View file

@ -1,38 +0,0 @@
package com.tangem.tap.domain.tasks
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.wallet.CreateWalletTask
@Deprecated("Use CreateProductWalletAndRescanTask instead")
class CreateWalletAndRescanTask : CardSessionRunnable<Card> {
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val card = session.environment.card.guard {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val firmwareVersion = card.firmwareVersion
val task = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
CreateWalletTask(card.supportedCurves.first())
} else {
CreateWalletsTask()
}
task.run(session) { result ->
when (result) {
is CompletionResult.Success ->
PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -1,49 +0,0 @@
package com.tangem.tap.domain.tasks
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.wallet.CreateWalletTask
@Deprecated("Use CreateProductWalletTask instead")
class CreateWalletsTask(curves: List<EllipticCurve>? = null) : CardSessionRunnable<Card> {
private val curves = curves ?: listOf(
EllipticCurve.Secp256k1,
EllipticCurve.Ed25519,
EllipticCurve.Secp256r1,
)
private var index = 0
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val curve = curves[index]
createWallet(curve, session, callback)
}
private fun createWallet(
curve: EllipticCurve,
session: CardSession,
callback: (result: CompletionResult<Card>) -> Unit,
) {
CreateWalletTask(curve).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
if (index == curves.lastIndex) {
PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback)
return@run
}
index += 1
createWallet(curves[index], session, callback)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
class CardInitializationValidator(private val expectedCurves: List<EllipticCurve>) {
fun validateWallets(wallets: List<CardWallet>): Boolean {
val curves = wallets.map { it.curve }.toSet()
return curves.size == expectedCurves.size &&
curves.containsAll(expectedCurves)
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemSdkError
@ -24,6 +25,7 @@ import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.backup.StartPrimaryCardLinkingCommand
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.operations.read.ReadWalletsListCommand
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.operations.wallet.CreateWalletResponse as SdkCreateWalletResponse
@ -60,6 +62,7 @@ class CreateProductWalletTask(
private val cardTypesResolver: CardTypesResolver,
private val derivationStyleProvider: DerivationStyleProvider,
private val mnemonic: Mnemonic? = null,
private val shouldReset: Boolean,
) : CardSessionRunnable<CreateProductWalletTaskResponse> {
override val allowsRequestAccessCodeFromRepository: Boolean = false
@ -79,7 +82,7 @@ class CreateProductWalletTask(
cardTypesResolver.isTangemTwins() ->
throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
else -> CreateWalletTangemWallet(mnemonic, derivationStyleProvider)
else -> CreateWalletTangemWallet(mnemonic, shouldReset, derivationStyleProvider, cardDto)
}
commandProcessor.proceed(cardDto, session) {
when (it) {
@ -139,38 +142,38 @@ private class CreateWalletTangemNote(private val cardTypesResolver: CardTypesRes
*/
private class CreateWalletTangemWallet(
private val mnemonic: Mnemonic?,
private val shouldReset: Boolean,
private val derivationStyleProvider: DerivationStyleProvider,
cardDTO: CardDTO,
) : ProductCommandProcessor<CreateProductWalletTaskResponse> {
private var primaryCard: PrimaryCard? = null
private val cardConfig = CardConfig.createConfig(cardDTO)
override fun proceed(
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
val config = CardConfig.createConfig(card)
val walletsOnCard = card.wallets.map { it.curve }.toSet()
val curves = config.mandatoryCurves.toSet()
.intersect(card.supportedCurves.toSet())
.subtract(walletsOnCard).toList()
if (curves.isEmpty()) {
val createWalletResponses = card.wallets.map { wallet ->
CreateWalletResponse(card.cardId, wallet)
}
proceedWithCreatedWallets(card, createWalletResponses, session, callback)
return
if (walletsOnCard.isEmpty()) {
createMultiWallet(card, session, callback)
} else if (shouldReset) {
resetCard(card, session, callback)
} else {
callback(CompletionResult.Failure(TangemSdkError.WalletAlreadyCreated()))
}
CreateWalletsTask(curves, mnemonic).run(session) { result ->
}
private fun createMultiWallet(
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
CreateWalletsTask(cardConfig.mandatoryCurves, mnemonic).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
proceedWithCreatedWallets(
card = card,
createWalletResponses = result.data.createWalletResponses.map { CreateWalletResponse(it) },
session = session,
callback = callback,
)
checkIfAllWalletsCreated(card, session, result.data, callback)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
@ -179,6 +182,61 @@ private class CreateWalletTangemWallet(
}
}
private fun checkIfAllWalletsCreated(
card: CardDTO,
session: CardSession,
createResponse: CreateWalletsResponse,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
if (card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
proceedWithCreatedWallets(
card = card,
createWalletResponses = createResponse.createWalletResponses.map { CreateWalletResponse(it) },
session = session,
callback = callback,
)
return
}
val command = ReadWalletsListCommand()
command.run(session) { response ->
when (response) {
is CompletionResult.Success -> {
val cardInitializationValidator = CardInitializationValidator(cardConfig.mandatoryCurves)
if (cardInitializationValidator.validateWallets(response.data.wallets)) {
proceedWithCreatedWallets(
card = card,
createWalletResponses = createResponse.createWalletResponses.map {
CreateWalletResponse(it)
},
session = session,
callback = callback,
)
} else {
callback(CompletionResult.Failure(TangemSdkError.WalletAlreadyCreated()))
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error))
}
}
}
private fun resetCard(
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
val resetCommand = ResetToFactorySettingsTask(allowsRequestAccessCodeFromRepository = false)
resetCommand.run(session) {
when (it) {
is CompletionResult.Success -> {
createMultiWallet(card, session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error))
}
}
}
private fun proceedWithCreatedWallets(
card: CardDTO,
createWalletResponses: List<CreateWalletResponse>,

View file

@ -9,4 +9,5 @@ import com.tangem.core.navigation.StateDialog
sealed class OnboardingDialog : StateDialog {
object TwinningProcessNotCompleted : OnboardingDialog()
data class InterruptOnboarding(val onOk: VoidCallback) : OnboardingDialog()
data class WalletActivationError(val onConfirm: () -> Unit) : OnboardingDialog()
}

View file

@ -120,7 +120,10 @@ private fun handleWalletAction(action: Action) {
is OnboardingWalletAction.CreateWallet -> {
scanResponse ?: return
scope.launch {
val result = tangemSdkManager.createProductWallet(scanResponse)
val result = tangemSdkManager.createProductWallet(
scanResponse,
globalState.onboardingState.shouldResetOnCreate,
)
store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(true, result))
}
}
@ -139,10 +142,15 @@ private fun handleWalletAction(action: Action) {
)
onboardingManager.scanResponse = updatedResponse
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false))
startCardActivation(updatedResponse)
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
is CompletionResult.Failure -> Unit
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
handleActivationError()
}
}
}
}
is OnboardingWalletAction.FinishOnboarding -> {
@ -218,9 +226,15 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
is OnboardingWallet2Action.CreateWallet -> {
scanResponse ?: return
scope.launch {
val mediateResult = when (val result = tangemSdkManager.createProductWallet(scanResponse)) {
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,
@ -230,6 +244,9 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
handleActivationError()
}
CompletionResult.Failure(result.error)
}
}
@ -246,6 +263,7 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
val result = tangemSdkManager.importWallet(
scanResponse = scanResponse,
mnemonic = action.mnemonicComponents.joinToString(" "),
shouldReset = globalState.onboardingState.shouldResetOnCreate,
)
) {
is CompletionResult.Success -> {
@ -259,6 +277,7 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
seedPhraseLength = action.mnemonicComponents.size,
),
)
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false))
val response = CreateWalletResponse(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
@ -268,6 +287,9 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
handleActivationError()
}
CompletionResult.Failure(result.error)
}
}
@ -300,6 +322,16 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
}
}
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

View file

@ -1,7 +1,7 @@
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.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
@ -15,7 +15,7 @@ import com.tangem.wallet.R
[REDACTED_AUTHOR]
*/
internal class OnboardingSeedPhraseStateHandler(
private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhrase(),
private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhraseScreen(),
) {
fun newState(

View file

@ -0,0 +1,27 @@
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.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.store
import com.tangem.wallet.R
object WalletActivationErrorDialog {
fun create(context: Context, dialog: OnboardingDialog.WalletActivationError): Dialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.onboarding_activation_error_title))
setMessage(context.getString(R.string.onboarding_activation_error_message))
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onConfirm() }
setNegativeButton(R.string.chat_button_title) { _, _ ->
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
}
setOnDismissListener { store.dispatchDialogHide() }
setCancelable(false)
}.create()
}
}

View file

@ -308,6 +308,8 @@
<string name="onboarding_access_code_repeat_code_title">Повторно введите код доступа</string>
<string name="onboarding_access_code_too_short">Код доступа должен состоять не менее чем из 4 символов.</string>
<string name="onboarding_access_codes_doesnt_match">Введенные коды доступа не совпадают</string>
<string name="onboarding_activation_error_message">Необходимо повторить операцию, при этом карта будет сброшена к заводским настройкам</string>
<string name="onboarding_activation_error_title">Ошибка активации</string>
<string name="onboarding_alert_message_not_max_backup_cards_added">Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить?</string>
<string name="onboarding_backup_exit_warning">Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас.</string>
<string name="onboarding_button_add_backup_card">Добавить резервную карту</string>

View file

@ -311,6 +311,8 @@
<string name="onboarding_access_code_repeat_code_title">Re-enter your Access Code</string>
<string name="onboarding_access_code_too_short">Access code must be at least 4 characters long</string>
<string name="onboarding_access_codes_doesnt_match">Entered access code didn\'t match the initial access code</string>
<string name="onboarding_activation_error_message">Please repeat the operation. The card will be reset to factory settings.</string>
<string name="onboarding_activation_error_title">Activation error</string>
<string name="onboarding_alert_message_not_max_backup_cards_added">You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process?</string>
<string name="onboarding_backup_exit_warning">The backup process is partly complete. You can\'t exit it now.</string>
<string name="onboarding_button_add_backup_card">Add a backup card</string>

View file

@ -14,7 +14,7 @@ import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseSc
/**
[REDACTED_AUTHOR]
*/
class OnboardingSeedPhrase : OnboardingSeedPhraseApi {
class OnboardingSeedPhraseScreen : OnboardingSeedPhraseApi {
@Composable
override fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float) {