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()
}
}