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(
scanResponse: ScanResponse,
crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,

View file

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

View file

@ -274,14 +274,16 @@ sealed class AppRoute(val path: String) : Route {
data class Onboarding(
val scanResponse: ScanResponse,
val mode: Mode = Mode.Onboarding,
) : AppRoute(path = "/onboarding_v2/${mode.name}") {
) : AppRoute(path = "/onboarding_v2/$mode") {
enum class Mode {
Onboarding, // general Mode
AddBackupWallet1, // continue backup process for existing wallet 1
WelcomeOnlyTwin, // show welcome screen and then navigate to wallet for twins
RecreateWalletTwin, // reset twins
ContinueFinalize, // continue finalize process (unfinished backup dialog)
@Serializable
sealed class Mode {
data object Onboarding : Mode() // general Mode
data object AddBackupWallet1 : Mode() // continue backup process for existing wallet 1
data object WelcomeOnlyTwin : Mode() // show welcome screen and then navigate to wallet for twins
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.domain.core.wallets.UserWalletsListRepository
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.HotWalletPasswordRequester
import com.tangem.domain.wallets.repository.WalletsRepository
@ -56,12 +57,22 @@ class DefaultHotWalletAccessor @Inject constructor(
contextualUnlockHotWallet[hotWalletId]
override fun clearContextualUnlock(hotWalletId: HotWalletId) {
contextualUnlockHotWallet[hotWalletId] = null
contextualUnlockHotWallet.remove(hotWalletId)
scope.launch {
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() {
val hotWalletsIds = contextualUnlockHotWallet.keys.toList()
contextualUnlockHotWallet.clear()

View file

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

View file

@ -2,6 +2,7 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.model.HotWalletId
@ -14,4 +15,10 @@ class ClearHotWalletContextualUnlockUseCase(
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.models)
implementation(projects.domain.settings)
implementation(projects.domain.feedback)
implementation(projects.domain.feedback.models)
/** Common */
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.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.hot.sdk.model.UnlockHotWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
@ -54,8 +53,6 @@ internal class AccessCodeModel @Inject constructor(
private val params = paramsContainer.require<AccessCodeComponent.Params>()
private var hotWalletId: HotWalletId? = null
internal val uiState: StateFlow<AccessCodeUM>
field = MutableStateFlow(getInitialState())
@ -197,7 +194,7 @@ internal class AccessCodeModel @Inject constructor(
}
override fun onDestroy() {
hotWalletId?.let { clearHotWalletContextualUnlockUseCase.invoke(it) }
clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
super.onDestroy()
}
}

View file

@ -1,33 +1,71 @@
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.model.Model
import com.tangem.core.decompose.model.ParamsContainer
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.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.features.hotwallet.UpgradeWalletComponent
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.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
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")
@ModelScoped
internal class UpgradeWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
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() {
private val params = paramsContainer.require<UpgradeWalletComponent.Params>()
internal val uiState: StateFlow<UpgradeWalletUM>
field = MutableStateFlow(
private val _uiState = MutableStateFlow(
UpgradeWalletUM(
onBackClick = { router.pop() },
onBuyTangemWalletClick = ::onBuyTangemWalletClick,
onScanDeviceClick = ::onScanDeviceClick,
),
)
internal val uiState: StateFlow<UpgradeWalletUM> = _uiState
override fun onDestroy() {
clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
super.onDestroy()
}
private fun onBuyTangemWalletClick() {
modelScope.launch {
@ -36,6 +74,73 @@ internal class UpgradeWalletModel @Inject constructor(
}
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 onBuyTangemWalletClick: () -> 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.features.hotwallet.ViewPhraseComponent
import com.tangem.features.hotwallet.viewphrase.entity.ViewPhraseUM
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
@ -35,8 +34,6 @@ internal class ViewPhraseModel @Inject constructor(
private val params = paramsContainer.require<ViewPhraseComponent.Params>()
private var hotWalletId: HotWalletId? = null
internal val uiState: StateFlow<ViewPhraseUM>
field = MutableStateFlow(
ViewPhraseUM(
@ -52,7 +49,6 @@ internal class ViewPhraseModel @Inject constructor(
val userWallet = getUserWalletUseCase(params.userWalletId)
.getOrElse { error("User wallet with id ${params.userWalletId} not found") }
if (userWallet is UserWallet.Hot) {
hotWalletId = userWallet.hotWalletId
modelScope.launch {
val words = exportSeedPhraseUseCase.invoke(userWallet.hotWalletId)
.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() {
hotWalletId?.let { clearHotWalletContextualUnlockUseCase.invoke(it) }
clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
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.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
interface OnboardingEntryComponent : ComposableContentComponent {
@ -11,12 +12,13 @@ interface OnboardingEntryComponent : ComposableContentComponent {
val mode: Mode,
)
enum class Mode {
Onboarding,
AddBackupWallet1,
WelcomeOnlyTwin,
RecreateWalletTwin,
ContinueFinalize,
sealed class Mode {
data object Onboarding : Mode()
data object AddBackupWallet1 : Mode()
data object WelcomeOnlyTwin : Mode()
data object RecreateWalletTwin : Mode()
data object ContinueFinalize : Mode()
data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode()
}
interface Factory : ComponentFactory<Params, OnboardingEntryComponent>

View file

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

View file

@ -73,16 +73,20 @@ internal class OnboardingEntryModel @Inject constructor(
uiMessageSender.send(CantLeaveBackupDialog)
}
@Suppress("CyclomaticComplexMethod")
private fun routeByProductType(scanResponse: ScanResponse): OnboardingRoute {
return when (scanResponse.productType) {
ProductType.Wallet,
ProductType.Wallet2,
ProductType.Ring,
-> {
val multiWalletNavigationMode = when (params.mode) {
Mode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding
Mode.AddBackupWallet1 -> OnboardingMultiWalletComponent.Mode.AddBackup
Mode.ContinueFinalize -> OnboardingMultiWalletComponent.Mode.ContinueFinalize
val multiWalletNavigationMode = when (val mode = params.mode) {
is Mode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding
is Mode.AddBackupWallet1 -> OnboardingMultiWalletComponent.Mode.AddBackup
is Mode.ContinueFinalize -> OnboardingMultiWalletComponent.Mode.ContinueFinalize
is Mode.UpgradeHotWallet -> OnboardingMultiWalletComponent.Mode.UpgradeHotWallet(
userWalletId = mode.userWalletId,
)
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.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.onboarding.v2.TitleProvider
interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavigationHolder {
@ -17,10 +18,11 @@ interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavi
val onDone: (UserWallet.Cold) -> Unit,
)
enum class Mode {
Onboarding,
AddBackup,
ContinueFinalize,
sealed class Mode {
data object Onboarding : Mode()
data object AddBackup : Mode()
data object ContinueFinalize : Mode()
data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode()
}
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.scanprimary.MultiWalletScanPrimaryComponent
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.OnboardingMultiWalletState
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") {
MutableStateFlow(
when (model.state.value.currentStep) {
UpgradeWallet -> WalletArtworksState.Folded
CreateWallet -> WalletArtworksState.Folded
ChooseBackupOption -> WalletArtworksState.Fan
SeedPhrase -> WalletArtworksState.Folded
@ -142,6 +144,11 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
childContext: AppComponentContext,
): ComposableContentComponent {
return when (step) {
UpgradeWallet -> MultiWalletUpgradeWalletComponent(
context = childContext,
params = childParams,
onNextStep = ::handleNavigationEvent,
)
CreateWallet -> MultiWalletCreateWalletComponent(
context = childContext,
params = childParams,

View file

@ -265,6 +265,16 @@ internal class MultiWalletFinalizeModel @Inject constructor(
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()
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.scanprimary.model.MultiWalletScanPrimaryModel
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 dagger.Binds
import dagger.Module
@ -72,4 +73,9 @@ internal interface ModelModule {
@IntoMap
@ClassKey(MultiWalletScanPrimaryModel::class)
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
return when {
params.mode is OnboardingMultiWalletComponent.Mode.UpgradeHotWallet -> {
OnboardingMultiWalletState.Step.UpgradeWallet
}
params.mode == OnboardingMultiWalletComponent.Mode.ContinueFinalize ->
OnboardingMultiWalletState.Step.Finalize
// Add backup button

View file

@ -32,7 +32,14 @@ data class OnboardingMultiWalletState(
* -> AddBackupDevice -> Finalize -> [Done]
*/
enum class Step {
CreateWallet, ChooseBackupOption, SeedPhrase, ScanPrimary, AddBackupDevice, Finalize, Done
UpgradeWallet,
CreateWallet,
ChooseBackupOption,
SeedPhrase,
ScanPrimary,
AddBackupDevice,
Finalize,
Done,
}
@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.Finalize -> resourceReference(R.string.onboarding_button_finalize_backup)
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) {
messageSender.send(makeBackupAtFirstAlertBS)
} else {
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 -> {
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))
}
}
}
unlockWalletIfNeedAndProceed {
router.push(AppRoute.UpdateAccessCode(params.userWalletId))
}
}
}
private fun onUpgradeWalletClick() {
router.push(AppRoute.UpgradeWallet(params.userWalletId))
unlockWalletIfNeedAndProceed {
router.push(AppRoute.UpgradeWallet(params.userWalletId))
}
}
private fun onDismissUpgradeWalletClick() {
@ -396,4 +380,28 @@ internal class WalletSettingsModel @Inject constructor(
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()
}
}
}
}
}
}