Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-11 15:53:28 +05:00
parent 410253dc5a
commit 3c3f35aeb6
26 changed files with 589 additions and 63 deletions

View file

@ -193,7 +193,7 @@ internal class LegacyScanProcessor @Inject constructor(
} }
} }
@Suppress("LongMethod", "MagicNumber") @Suppress("LongMethod", "LongParameterList", "MagicNumber")
private suspend inline fun onScanSuccess( private suspend inline fun onScanSuccess(
scanResponse: ScanResponse, scanResponse: ScanResponse,
crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit, crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,

View file

@ -237,14 +237,19 @@ internal class ChildFactory @Inject constructor(
context = context, context = context,
params = OnboardingEntryComponent.Params( params = OnboardingEntryComponent.Params(
scanResponse = route.scanResponse, scanResponse = route.scanResponse,
mode = when (route.mode) { mode = when (val mode = route.mode) {
AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.Mode.Onboarding is AppRoute.Onboarding.Mode.Onboarding ->
AppRoute.Onboarding.Mode.AddBackupWallet1 -> OnboardingEntryComponent.Mode.AddBackupWallet1 OnboardingEntryComponent.Mode.Onboarding
AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin is AppRoute.Onboarding.Mode.AddBackupWallet1 ->
AppRoute.Onboarding.Mode.RecreateWalletTwin -> OnboardingEntryComponent.Mode.AddBackupWallet1
is AppRoute.Onboarding.Mode.WelcomeOnlyTwin ->
OnboardingEntryComponent.Mode.WelcomeOnlyTwin
is AppRoute.Onboarding.Mode.RecreateWalletTwin ->
OnboardingEntryComponent.Mode.RecreateWalletTwin OnboardingEntryComponent.Mode.RecreateWalletTwin
AppRoute.Onboarding.Mode.ContinueFinalize -> is AppRoute.Onboarding.Mode.ContinueFinalize ->
OnboardingEntryComponent.Mode.ContinueFinalize OnboardingEntryComponent.Mode.ContinueFinalize
is AppRoute.Onboarding.Mode.UpgradeHotWallet ->
OnboardingEntryComponent.Mode.UpgradeHotWallet(mode.userWalletId)
}, },
), ),
componentFactory = onboardingEntryComponentFactory, componentFactory = onboardingEntryComponentFactory,

View file

@ -274,14 +274,16 @@ sealed class AppRoute(val path: String) : Route {
data class Onboarding( data class Onboarding(
val scanResponse: ScanResponse, val scanResponse: ScanResponse,
val mode: Mode = Mode.Onboarding, val mode: Mode = Mode.Onboarding,
) : AppRoute(path = "/onboarding_v2/${mode.name}") { ) : AppRoute(path = "/onboarding_v2/$mode") {
enum class Mode { @Serializable
Onboarding, // general Mode sealed class Mode {
AddBackupWallet1, // continue backup process for existing wallet 1 data object Onboarding : Mode() // general Mode
WelcomeOnlyTwin, // show welcome screen and then navigate to wallet for twins data object AddBackupWallet1 : Mode() // continue backup process for existing wallet 1
RecreateWalletTwin, // reset twins data object WelcomeOnlyTwin : Mode() // show welcome screen and then navigate to wallet for twins
ContinueFinalize, // continue finalize process (unfinished backup dialog) data object RecreateWalletTwin : Mode() // reset twins
data object ContinueFinalize : Mode() // continue finalize process (unfinished backup dialog)
data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode() // upgrade hot wallet
} }
} }

View file

@ -3,6 +3,7 @@ package com.tangem.data.wallets.hot
import com.tangem.common.core.TangemSdkError import com.tangem.common.core.TangemSdkError
import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.hot.HotWalletAccessor import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.repository.WalletsRepository
@ -56,12 +57,22 @@ class DefaultHotWalletAccessor @Inject constructor(
contextualUnlockHotWallet[hotWalletId] contextualUnlockHotWallet[hotWalletId]
override fun clearContextualUnlock(hotWalletId: HotWalletId) { override fun clearContextualUnlock(hotWalletId: HotWalletId) {
contextualUnlockHotWallet[hotWalletId] = null contextualUnlockHotWallet.remove(hotWalletId)
scope.launch { scope.launch {
tangemHotSdk.clearUnlockContext(hotWalletId) tangemHotSdk.clearUnlockContext(hotWalletId)
} }
} }
override fun clearContextualUnlock(userWalletId: UserWalletId) {
scope.launch {
val userWallet = userWalletsListRepository.userWalletsSync()
.find { it is UserWallet.Hot && it.walletId == userWalletId }
as? UserWallet.Hot
?: return@launch
clearContextualUnlock(userWallet.hotWalletId)
}
}
override fun clearAllContextualUnlock() { override fun clearAllContextualUnlock() {
val hotWalletsIds = contextualUnlockHotWallet.keys.toList() val hotWalletsIds = contextualUnlockHotWallet.keys.toList()
contextualUnlockHotWallet.clear() contextualUnlockHotWallet.clear()

View file

@ -1,5 +1,6 @@
package com.tangem.domain.wallets.hot package com.tangem.domain.wallets.hot
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.hot.sdk.model.* import com.tangem.hot.sdk.model.*
interface HotWalletAccessor { interface HotWalletAccessor {
@ -16,5 +17,7 @@ interface HotWalletAccessor {
fun clearContextualUnlock(hotWalletId: HotWalletId) fun clearContextualUnlock(hotWalletId: HotWalletId)
fun clearContextualUnlock(userWalletId: UserWalletId)
fun clearAllContextualUnlock() fun clearAllContextualUnlock()
} }

View file

@ -2,6 +2,7 @@
package com.tangem.domain.wallets.usecase package com.tangem.domain.wallets.usecase
import arrow.core.Either import arrow.core.Either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.hot.HotWalletAccessor import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.model.HotWalletId import com.tangem.hot.sdk.model.HotWalletId
@ -14,4 +15,10 @@ class ClearHotWalletContextualUnlockUseCase(
hotWalletAccessor.clearContextualUnlock(hotWalletId) hotWalletAccessor.clearContextualUnlock(hotWalletId)
} }
} }
operator fun invoke(userWalletId: UserWalletId): Either<Throwable, Unit> {
return Either.catch {
hotWalletAccessor.clearContextualUnlock(userWalletId)
}
}
} }

View file

@ -34,6 +34,8 @@ dependencies {
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.settings) implementation(projects.domain.settings)
implementation(projects.domain.feedback)
implementation(projects.domain.feedback.models)
/** Common */ /** Common */
implementation(projects.common.ui) implementation(projects.common.ui)

View file

@ -23,7 +23,6 @@ import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM
import com.tangem.features.hotwallet.impl.R import com.tangem.features.hotwallet.impl.R
import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.HotAuth import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.hot.sdk.model.UnlockHotWallet import com.tangem.hot.sdk.model.UnlockHotWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -54,8 +53,6 @@ internal class AccessCodeModel @Inject constructor(
private val params = paramsContainer.require<AccessCodeComponent.Params>() private val params = paramsContainer.require<AccessCodeComponent.Params>()
private var hotWalletId: HotWalletId? = null
internal val uiState: StateFlow<AccessCodeUM> internal val uiState: StateFlow<AccessCodeUM>
field = MutableStateFlow(getInitialState()) field = MutableStateFlow(getInitialState())
@ -197,7 +194,7 @@ internal class AccessCodeModel @Inject constructor(
} }
override fun onDestroy() { override fun onDestroy() {
hotWalletId?.let { clearHotWalletContextualUnlockUseCase.invoke(it) } clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
super.onDestroy() super.onDestroy()
} }
} }

View file

@ -1,33 +1,71 @@
package com.tangem.features.hotwallet.upgradewallet package com.tangem.features.hotwallet.upgradewallet
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.usecase.ClearHotWalletContextualUnlockUseCase
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.features.hotwallet.UpgradeWalletComponent
import com.tangem.features.hotwallet.upgradewallet.entity.UpgradeWalletUM import com.tangem.features.hotwallet.upgradewallet.entity.UpgradeWalletUM
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped @ModelScoped
internal class UpgradeWalletModel @Inject constructor( internal class UpgradeWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val router: Router, private val router: Router,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener, private val urlOpener: UrlOpener,
private val settingsRepository: SettingsRepository,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val uiMessageSender: UiMessageSender,
private val clearHotWalletContextualUnlockUseCase: ClearHotWalletContextualUnlockUseCase,
private val tangemSdkManager: TangemSdkManager,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
) : Model() { ) : Model() {
private val params = paramsContainer.require<UpgradeWalletComponent.Params>()
internal val uiState: StateFlow<UpgradeWalletUM> private val _uiState = MutableStateFlow(
field = MutableStateFlow(
UpgradeWalletUM( UpgradeWalletUM(
onBackClick = { router.pop() }, onBackClick = { router.pop() },
onBuyTangemWalletClick = ::onBuyTangemWalletClick, onBuyTangemWalletClick = ::onBuyTangemWalletClick,
onScanDeviceClick = ::onScanDeviceClick, onScanDeviceClick = ::onScanDeviceClick,
), ),
) )
internal val uiState: StateFlow<UpgradeWalletUM> = _uiState
override fun onDestroy() {
clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
super.onDestroy()
}
private fun onBuyTangemWalletClick() { private fun onBuyTangemWalletClick() {
modelScope.launch { modelScope.launch {
@ -36,6 +74,73 @@ internal class UpgradeWalletModel @Inject constructor(
} }
private fun onScanDeviceClick() { private fun onScanDeviceClick() {
// TODO [REDACTED_TASK_KEY] scanCard()
}
private fun scanCard() {
modelScope.launch {
setLoading(true)
val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes()
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes,
)
tangemSdkManager
.scanProduct()
.doOnSuccess {
delay(DELAY_SDK_DIALOG_CLOSE)
tangemSdkManager.changeDisplayedCardIdNumbersCount(it)
navigateToUpgradeFlow(it)
}
.doOnFailure {
showCardVerificationFailedDialog(it)
}
}
}
private fun setLoading(isLoading: Boolean) {
_uiState.update { it.copy(isLoading = isLoading) }
}
private fun showCardVerificationFailedDialog(error: TangemError) {
if (error !is TangemSdkError.CardVerificationFailed) return
// TODO [REDACTED_TASK_KEY] track error
val resource = error.localizedDescriptionRes()
val resId = resource.resId ?: R.string.common_unknown_error
val resArgs = resource.args.map { it.value }
uiMessageSender.send(
DialogMessage(
message = resourceReference(id = resId, resArgs.toWrappedList()),
title = resourceReference(id = R.string.security_alert_title),
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(id = R.string.alert_button_request_support),
onClick = {
modelScope.launch {
sendFeedbackEmailUseCase(type = FeedbackEmailType.CardAttestationFailed)
}
},
)
},
secondActionBuilder = { cancelAction(onClick = {}) },
),
)
}
private fun navigateToUpgradeFlow(scanResponse: ScanResponse) {
setLoading(false)
router.push(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.UpgradeHotWallet(
userWalletId = params.userWalletId,
),
),
)
} }
} }

View file

@ -4,4 +4,5 @@ internal data class UpgradeWalletUM(
val onBackClick: () -> Unit, val onBackClick: () -> Unit,
val onBuyTangemWalletClick: () -> Unit, val onBuyTangemWalletClick: () -> Unit,
val onScanDeviceClick: () -> Unit, val onScanDeviceClick: () -> Unit,
val isLoading: Boolean = false,
) )

View file

@ -13,7 +13,6 @@ import com.tangem.domain.wallets.usecase.ExportSeedPhraseUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.hotwallet.ViewPhraseComponent import com.tangem.features.hotwallet.ViewPhraseComponent
import com.tangem.features.hotwallet.viewphrase.entity.ViewPhraseUM import com.tangem.features.hotwallet.viewphrase.entity.ViewPhraseUM
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -35,8 +34,6 @@ internal class ViewPhraseModel @Inject constructor(
private val params = paramsContainer.require<ViewPhraseComponent.Params>() private val params = paramsContainer.require<ViewPhraseComponent.Params>()
private var hotWalletId: HotWalletId? = null
internal val uiState: StateFlow<ViewPhraseUM> internal val uiState: StateFlow<ViewPhraseUM>
field = MutableStateFlow( field = MutableStateFlow(
ViewPhraseUM( ViewPhraseUM(
@ -52,7 +49,6 @@ internal class ViewPhraseModel @Inject constructor(
val userWallet = getUserWalletUseCase(params.userWalletId) val userWallet = getUserWalletUseCase(params.userWalletId)
.getOrElse { error("User wallet with id ${params.userWalletId} not found") } .getOrElse { error("User wallet with id ${params.userWalletId} not found") }
if (userWallet is UserWallet.Hot) { if (userWallet is UserWallet.Hot) {
hotWalletId = userWallet.hotWalletId
modelScope.launch { modelScope.launch {
val words = exportSeedPhraseUseCase.invoke(userWallet.hotWalletId) val words = exportSeedPhraseUseCase.invoke(userWallet.hotWalletId)
.getOrElse { error("Unable to export seed phrase for wallet with id ${params.userWalletId}") } .getOrElse { error("Unable to export seed phrase for wallet with id ${params.userWalletId}") }
@ -70,7 +66,7 @@ internal class ViewPhraseModel @Inject constructor(
} }
override fun onDestroy() { override fun onDestroy() {
hotWalletId?.let { clearHotWalletContextualUnlockUseCase.invoke(it) } clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
super.onDestroy() super.onDestroy()
} }
} }

View file

@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.entry
import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
interface OnboardingEntryComponent : ComposableContentComponent { interface OnboardingEntryComponent : ComposableContentComponent {
@ -11,12 +12,13 @@ interface OnboardingEntryComponent : ComposableContentComponent {
val mode: Mode, val mode: Mode,
) )
enum class Mode { sealed class Mode {
Onboarding, data object Onboarding : Mode()
AddBackupWallet1, data object AddBackupWallet1 : Mode()
WelcomeOnlyTwin, data object WelcomeOnlyTwin : Mode()
RecreateWalletTwin, data object RecreateWalletTwin : Mode()
ContinueFinalize, data object ContinueFinalize : Mode()
data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode()
} }
interface Factory : ComponentFactory<Params, OnboardingEntryComponent> interface Factory : ComponentFactory<Params, OnboardingEntryComponent>

View file

@ -53,6 +53,7 @@ dependencies {
implementation(projects.domain.transaction) implementation(projects.domain.transaction)
/** Tangem libraries */ /** Tangem libraries */
implementation(tangemDeps.hot.core)
implementation(projects.libs.tangemSdkApi) implementation(projects.libs.tangemSdkApi)
implementation(tangemDeps.card.core) implementation(tangemDeps.card.core)
implementation(tangemDeps.card.android) { implementation(tangemDeps.card.android) {

View file

@ -73,16 +73,20 @@ internal class OnboardingEntryModel @Inject constructor(
uiMessageSender.send(CantLeaveBackupDialog) uiMessageSender.send(CantLeaveBackupDialog)
} }
@Suppress("CyclomaticComplexMethod")
private fun routeByProductType(scanResponse: ScanResponse): OnboardingRoute { private fun routeByProductType(scanResponse: ScanResponse): OnboardingRoute {
return when (scanResponse.productType) { return when (scanResponse.productType) {
ProductType.Wallet, ProductType.Wallet,
ProductType.Wallet2, ProductType.Wallet2,
ProductType.Ring, ProductType.Ring,
-> { -> {
val multiWalletNavigationMode = when (params.mode) { val multiWalletNavigationMode = when (val mode = params.mode) {
Mode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding is Mode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding
Mode.AddBackupWallet1 -> OnboardingMultiWalletComponent.Mode.AddBackup is Mode.AddBackupWallet1 -> OnboardingMultiWalletComponent.Mode.AddBackup
Mode.ContinueFinalize -> OnboardingMultiWalletComponent.Mode.ContinueFinalize is Mode.ContinueFinalize -> OnboardingMultiWalletComponent.Mode.ContinueFinalize
is Mode.UpgradeHotWallet -> OnboardingMultiWalletComponent.Mode.UpgradeHotWallet(
userWalletId = mode.userWalletId,
)
else -> error("Incorrect onboarding type") else -> error("Incorrect onboarding type")
} }

View file

@ -5,6 +5,7 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder
import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.onboarding.v2.TitleProvider import com.tangem.features.onboarding.v2.TitleProvider
interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavigationHolder { interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavigationHolder {
@ -17,10 +18,11 @@ interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavi
val onDone: (UserWallet.Cold) -> Unit, val onDone: (UserWallet.Cold) -> Unit,
) )
enum class Mode { sealed class Mode {
Onboarding, data object Onboarding : Mode()
AddBackup, data object AddBackup : Mode()
ContinueFinalize, data object ContinueFinalize : Mode()
data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode()
} }
interface Factory : ComponentFactory<Params, OnboardingMultiWalletComponent> interface Factory : ComponentFactory<Params, OnboardingMultiWalletComponent>

View file

@ -35,6 +35,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.Mul
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.MultiWalletFinalizeComponent import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.MultiWalletFinalizeComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.MultiWalletScanPrimaryComponent import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.MultiWalletScanPrimaryComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.MultiWalletSeedPhraseComponent import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.MultiWalletSeedPhraseComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.MultiWalletUpgradeWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.Step.* import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.Step.*
@ -57,6 +58,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
private val artworksState = instanceKeeper.getOrCreateSimple(key = "artworksState") { private val artworksState = instanceKeeper.getOrCreateSimple(key = "artworksState") {
MutableStateFlow( MutableStateFlow(
when (model.state.value.currentStep) { when (model.state.value.currentStep) {
UpgradeWallet -> WalletArtworksState.Folded
CreateWallet -> WalletArtworksState.Folded CreateWallet -> WalletArtworksState.Folded
ChooseBackupOption -> WalletArtworksState.Fan ChooseBackupOption -> WalletArtworksState.Fan
SeedPhrase -> WalletArtworksState.Folded SeedPhrase -> WalletArtworksState.Folded
@ -142,6 +144,11 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
childContext: AppComponentContext, childContext: AppComponentContext,
): ComposableContentComponent { ): ComposableContentComponent {
return when (step) { return when (step) {
UpgradeWallet -> MultiWalletUpgradeWalletComponent(
context = childContext,
params = childParams,
onNextStep = ::handleNavigationEvent,
)
CreateWallet -> MultiWalletCreateWalletComponent( CreateWallet -> MultiWalletCreateWalletComponent(
context = childContext, context = childContext,
params = childParams, params = childParams,

View file

@ -265,6 +265,16 @@ internal class MultiWalletFinalizeModel @Inject constructor(
userWallet userWallet
} }
is OnboardingMultiWalletComponent.Mode.UpgradeHotWallet -> {
saveWalletUseCase(
userWallet = userWalletCreated.copy(
scanResponse = scanResponse.updateScanResponseAfterBackup(),
),
canOverride = true,
)
// TODO [REDACTED_TASK_KEY] remove hot wallet after upgrade
userWalletCreated
}
}.requireColdWallet() }.requireColdWallet()
if (hasRing) { if (hasRing) {

View file

@ -0,0 +1,56 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.essenty.lifecycle.doOnStart
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.model.MultiWalletUpgradeWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.ui.MultiWalletUpgradeWallet
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
internal class MultiWalletUpgradeWalletComponent(
context: AppComponentContext,
params: MultiWalletChildParams,
onNextStep: (OnboardingMultiWalletState.Step) -> Unit,
) : AppComponentContext by context, MultiWalletChildComponent {
private val model: MultiWalletUpgradeWalletModel = getOrCreateModel(params)
init {
lifecycle.doOnStart {
params.innerNavigation.update {
it.copy(
stackSize = 2,
stackMaxSize = 9,
)
}
params.parentParams.titleProvider.changeTitle(
text = resourceReference(R.string.common_tangem),
)
}
componentScope.launch {
model.onDone.collect(onNextStep)
}
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
MultiWalletUpgradeWallet(
modifier = modifier,
state = state,
)
}
}

View file

@ -0,0 +1,181 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.ExportSeedPhraseUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.ui.state.MultiWalletUpgradeWalletUM
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.Step
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class MultiWalletUpgradeWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val cardRepository: CardRepository,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val saveWalletUseCase: SaveWalletUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val exportSeedPhraseUseCase: ExportSeedPhraseUseCase,
) : Model() {
private val params = paramsContainer.require<MultiWalletChildParams>()
private val multiWalletState
get() = params.multiWalletState
private val _uiState = MutableStateFlow(
MultiWalletUpgradeWalletUM(
title = resourceReference(R.string.hw_upgrade_start_title),
bodyText = resourceReference(R.string.hw_upgrade_start_description),
onStartUpgradeClick = {
// TODO [REDACTED_TASK_KEY] track button click
upgradeWallet(false)
},
dialog = null,
),
)
val uiState: StateFlow<MultiWalletUpgradeWalletUM> = _uiState
val onDone = MutableSharedFlow<Step>()
init {
// TODO [REDACTED_TASK_KEY] track screen opened
}
private fun upgradeWallet(shouldReset: Boolean) {
modelScope.launch {
val mode = params.parentParams.mode
require(mode is OnboardingMultiWalletComponent.Mode.UpgradeHotWallet)
val userWallet = getUserWalletUseCase(mode.userWalletId)
.getOrElse { error("User wallet with id ${mode.userWalletId} not found") }
if (userWallet is UserWallet.Hot) {
val privateInfo = exportSeedPhraseUseCase
.invoke(userWallet.hotWalletId)
.getOrElse { error("Unable to export seed phrase for wallet with id ${mode.userWalletId}") }
modelScope.launch {
val result = tangemSdkManager.importWallet(
scanResponse = multiWalletState.value.currentScanResponse,
shouldReset = shouldReset,
mnemonic = privateInfo.mnemonic.mnemonicComponents.joinToString(" "),
passphrase = privateInfo.passphrase?.concatToString(),
)
when (result) {
is CompletionResult.Success -> {
multiWalletState.update {
it.copy(
currentScanResponse = it.currentScanResponse.copy(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
),
)
}
cardRepository.startCardActivation(cardId = result.data.card.cardId)
// TODO [REDACTED_TASK_KEY] track wallet created
val cardDoesNotSupportBackup = result.data.card.settings.isBackupAllowed.not()
when {
cardDoesNotSupportBackup -> createWalletAndNavigateBackWithDone()
params.parentParams.withSeedPhraseFlow -> onDone.emit(Step.AddBackupDevice)
else -> {
onDone.emit(Step.ChooseBackupOption)
}
}
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
// show should reset dialog
handleActivationError()
}
}
}
}
}
}
}
private fun createWalletAndNavigateBackWithDone() {
modelScope.launch {
val scanResponse = params.multiWalletState.value.currentScanResponse
val userWallet = createUserWallet(scanResponse)
saveWalletUseCase(userWallet, canOverride = true)
.onRight {
cardRepository.finishCardActivation(scanResponse.card.cardId)
// save user wallet for manage tokens screen
params.multiWalletState.update {
it.copy(resultUserWallet = userWallet)
}
onDone.emit(Step.Done)
}
}
}
private fun createUserWallet(scanResponse: ScanResponse): UserWallet.Cold {
return requireNotNull(
value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(),
lazyMessage = { "User wallet not created" },
)
}
private fun handleActivationError() {
_uiState.update { state ->
state.copy(
dialog = resetCardDialog(
onConfirm = ::navigateToSupportScreen,
dismiss = { _uiState.update { it.copy(dialog = null) } },
onDismissButtonClick = ::resetCard,
),
)
}
}
private fun resetCard() {
upgradeWallet(true)
}
fun navigateToSupportScreen() {
modelScope.launch {
val cardInfo =
getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))
}
}
}

View file

@ -0,0 +1,98 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.ui.state.MultiWalletUpgradeWalletUM
@Composable
internal fun MultiWalletUpgradeWallet(state: MultiWalletUpgradeWalletUM, modifier: Modifier = Modifier) {
if (state.dialog != null) {
BasicDialog(
title = state.dialog.title.resolveReference(),
message = state.dialog.message.resolveReference(),
confirmButton = DialogButtonUM(
title = state.dialog.confirmButtonText.resolveReference(),
onClick = state.dialog.onConfirmClick,
),
dismissButton = DialogButtonUM(
title = state.dialog.dismissButtonText.resolveReference(),
warning = state.dialog.dismissWarningColor,
onClick = state.dialog.onDismissButtonClick,
),
onDismissDialog = state.dialog.onDismiss,
)
}
Column(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding(),
verticalArrangement = Arrangement.Bottom,
) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(start = 32.dp, end = 32.dp, bottom = 16.dp)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = state.title.resolveReference(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp),
)
Text(
text = state.bodyText.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 12.dp),
)
}
PrimaryButtonIconEnd(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
iconResId = R.drawable.ic_tangem_24,
text = stringResourceSafe(R.string.hw_upgrade_start_action),
onClick = state.onStartUpgradeClick,
)
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
MultiWalletUpgradeWallet(
state = MultiWalletUpgradeWalletUM(
title = stringReference("Title"),
bodyText = stringReference("Body body body"),
onStartUpgradeClick = {},
dialog = null,
),
)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.ui.state
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
internal data class MultiWalletUpgradeWalletUM(
val title: TextReference,
val bodyText: TextReference,
val onStartUpgradeClick: () -> Unit,
val dialog: OnboardingDialogUM?,
)

View file

@ -11,6 +11,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.mod
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model.MultiWalletFinalizeModel import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model.MultiWalletFinalizeModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.model.MultiWalletScanPrimaryModel import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.model.MultiWalletScanPrimaryModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.MultiWalletSeedPhraseModel import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.MultiWalletSeedPhraseModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.model.MultiWalletUpgradeWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
@ -72,4 +73,9 @@ internal interface ModelModule {
@IntoMap @IntoMap
@ClassKey(MultiWalletScanPrimaryModel::class) @ClassKey(MultiWalletScanPrimaryModel::class)
fun provideModel8(model: MultiWalletScanPrimaryModel): Model fun provideModel8(model: MultiWalletScanPrimaryModel): Model
@Binds
@IntoMap
@ClassKey(MultiWalletUpgradeWalletModel::class)
fun provideModel9(model: MultiWalletUpgradeWalletModel): Model
} }

View file

@ -117,6 +117,9 @@ internal class OnboardingMultiWalletModel @Inject constructor(
val card = scanResponse.card val card = scanResponse.card
return when { return when {
params.mode is OnboardingMultiWalletComponent.Mode.UpgradeHotWallet -> {
OnboardingMultiWalletState.Step.UpgradeWallet
}
params.mode == OnboardingMultiWalletComponent.Mode.ContinueFinalize -> params.mode == OnboardingMultiWalletComponent.Mode.ContinueFinalize ->
OnboardingMultiWalletState.Step.Finalize OnboardingMultiWalletState.Step.Finalize
// Add backup button // Add backup button

View file

@ -32,7 +32,14 @@ data class OnboardingMultiWalletState(
* -> AddBackupDevice -> Finalize -> [Done] * -> AddBackupDevice -> Finalize -> [Done]
*/ */
enum class Step { enum class Step {
CreateWallet, ChooseBackupOption, SeedPhrase, ScanPrimary, AddBackupDevice, Finalize, Done UpgradeWallet,
CreateWallet,
ChooseBackupOption,
SeedPhrase,
ScanPrimary,
AddBackupDevice,
Finalize,
Done,
} }
@JvmInline @JvmInline

View file

@ -14,4 +14,5 @@ fun screenTitleByStep(step: OnboardingMultiWalletState.Step): TextReference = wh
OnboardingMultiWalletState.Step.ChooseBackupOption -> resourceReference(R.string.onboarding_getting_started) OnboardingMultiWalletState.Step.ChooseBackupOption -> resourceReference(R.string.onboarding_getting_started)
OnboardingMultiWalletState.Step.Finalize -> resourceReference(R.string.onboarding_button_finalize_backup) OnboardingMultiWalletState.Step.Finalize -> resourceReference(R.string.onboarding_button_finalize_backup)
OnboardingMultiWalletState.Step.Done -> resourceReference(R.string.common_done) OnboardingMultiWalletState.Step.Done -> resourceReference(R.string.common_done)
OnboardingMultiWalletState.Step.UpgradeWallet -> resourceReference(R.string.common_tangem)
} }

View file

@ -363,32 +363,16 @@ internal class WalletSettingsModel @Inject constructor(
if (!state.value.isWalletBackedUp) { if (!state.value.isWalletBackedUp) {
messageSender.send(makeBackupAtFirstAlertBS) messageSender.send(makeBackupAtFirstAlertBS)
} else { } else {
val userWallet = getUserWalletUseCase(params.userWalletId) unlockWalletIfNeedAndProceed {
.getOrElse { error("User wallet with id ${params.userWalletId} not found") } router.push(AppRoute.UpdateAccessCode(params.userWalletId))
if (userWallet is UserWallet.Hot) {
val hotWalletId = userWallet.hotWalletId
when (hotWalletId.authType) {
HotWalletId.AuthType.NoPassword -> {
router.push(AppRoute.UpdateAccessCode(params.userWalletId))
}
HotWalletId.AuthType.Password,
HotWalletId.AuthType.Biometry,
-> modelScope.launch {
unlockHotWalletContextualUseCase.invoke(hotWalletId)
.onLeft {
Timber.e("Unable to unlock wallet with id ${params.userWalletId}")
}
.onRight {
router.push(AppRoute.UpdateAccessCode(params.userWalletId))
}
}
}
} }
} }
} }
private fun onUpgradeWalletClick() { private fun onUpgradeWalletClick() {
router.push(AppRoute.UpgradeWallet(params.userWalletId)) unlockWalletIfNeedAndProceed {
router.push(AppRoute.UpgradeWallet(params.userWalletId))
}
} }
private fun onDismissUpgradeWalletClick() { private fun onDismissUpgradeWalletClick() {
@ -396,4 +380,28 @@ internal class WalletSettingsModel @Inject constructor(
dismissUpgradeWalletNotificationUseCase.invoke(params.userWalletId) dismissUpgradeWalletNotificationUseCase.invoke(params.userWalletId)
} }
} }
private fun unlockWalletIfNeedAndProceed(action: () -> Unit) {
val userWallet = getUserWalletUseCase(params.userWalletId)
.getOrElse { error("User wallet with id ${params.userWalletId} not found") }
if (userWallet is UserWallet.Hot) {
val hotWalletId = userWallet.hotWalletId
when (hotWalletId.authType) {
HotWalletId.AuthType.NoPassword -> {
action()
}
HotWalletId.AuthType.Password,
HotWalletId.AuthType.Biometry,
-> modelScope.launch {
unlockHotWalletContextualUseCase.invoke(hotWalletId)
.onLeft {
Timber.e("Unable to unlock wallet with id ${params.userWalletId}")
}
.onRight {
action()
}
}
}
}
}
} }