Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-06 15:07:39 +05:00
parent 37486f3877
commit d84c61dc58
13 changed files with 23 additions and 493 deletions

View file

@ -542,6 +542,7 @@
<string name="hw_create_seed_description">Create or restore your wallet using a recovery phrase — your built-in backup.</string>
<string name="hw_create_seed_title">Seed phrase backup</string>
<string name="hw_create_title">Create Mobile Wallet</string>
<string name="hw_import_existing_wallet">Import existing wallet</string>
<string name="hw_import_seed_phrase_already_imported">This recovery phrase has already been imported</string>
<string name="hw_mobile_wallet">Mobile Wallet</string>
<string name="hw_upgrade_error_card_already_has_wallet">Cant upgrade. A wallet already exists on this device.</string>

View file

@ -15,7 +15,6 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
import com.tangem.features.hotwallet.accesscode.AccessCodeComponent
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
@ -36,7 +35,6 @@ internal class AddExistingWalletModel @Inject constructor(
) : Model() {
val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback()
val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks()
val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks()
val manualBackupCompletedComponentModelCallbacks = ManualBackupCompletedComponentModelCallbacks()
val accessCodeModelCallbacks = AccessCodeModelCallbacks()
@ -44,13 +42,12 @@ internal class AddExistingWalletModel @Inject constructor(
val mobileWalletSetupFinishedComponentModelCallbacks = MobileWalletSetupFinishedComponentModelCallbacks()
val stackNavigation = StackNavigation<AddExistingWalletRoute>()
val startRoute = AddExistingWalletRoute.Start
val startRoute = AddExistingWalletRoute.Import
val currentRoute: MutableStateFlow<AddExistingWalletRoute> = MutableStateFlow(startRoute)
fun onChildBack() {
when (currentRoute.value) {
is AddExistingWalletRoute.Start -> router.pop()
is AddExistingWalletRoute.Import -> stackNavigation.pop()
is AddExistingWalletRoute.Import -> router.pop()
is AddExistingWalletRoute.BackupCompleted -> Unit
is AddExistingWalletRoute.SetAccessCode -> Unit
is AddExistingWalletRoute.ConfirmAccessCode -> stackNavigation.pop()
@ -102,16 +99,6 @@ internal class AddExistingWalletModel @Inject constructor(
}
}
inner class AddExistingWalletStartModelCallbacks : AddExistingWalletStartComponent.ModelCallbacks {
override fun onBackClick() {
router.pop()
}
override fun onImportPhraseClick() {
stackNavigation.push(AddExistingWalletRoute.Import)
}
}
inner class AddExistingWalletImportModelCallbacks : AddExistingWalletImportComponent.ModelCallbacks {
override fun onWalletImported(userWalletId: UserWalletId) {
stackNavigation.replaceAll(AddExistingWalletRoute.BackupCompleted(userWalletId))

View file

@ -9,8 +9,6 @@ internal class AddExistingWalletStepperStateManager {
fun getStepperState(route: AddExistingWalletRoute): HotWalletStepperComponent.StepperUM? {
return when (route) {
is AddExistingWalletRoute.Start -> null
is AddExistingWalletRoute.Import -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_IMPORT,
steps = STEPS_COUNT,

View file

@ -5,7 +5,6 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletModel
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
import com.tangem.features.hotwallet.accesscode.AccessCodeComponent
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
@ -24,12 +23,6 @@ internal class AddExistingWalletChildFactory @Inject constructor(
model: AddExistingWalletModel,
): ComposableContentComponent {
return when (route) {
is AddExistingWalletRoute.Start -> AddExistingWalletStartComponent(
context = childContext,
params = AddExistingWalletStartComponent.Params(
callbacks = model.addExistingWalletStartModelCallbacks,
),
)
is AddExistingWalletRoute.Import -> AddExistingWalletImportComponent(
context = childContext,
params = AddExistingWalletImportComponent.Params(

View file

@ -6,9 +6,6 @@ import kotlinx.serialization.Serializable
internal sealed class AddExistingWalletRoute : Route {
@Serializable
object Start : AddExistingWalletRoute()
@Serializable
object Import : AddExistingWalletRoute()

View file

@ -1,37 +0,0 @@
package com.tangem.features.hotwallet.addexistingwallet.start
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.addexistingwallet.start.ui.AddExistingWalletStartContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
internal class AddExistingWalletStartComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val model: AddExistingWalletStartModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
AddExistingWalletStartContent(
state = state,
modifier = modifier,
)
}
interface ModelCallbacks {
fun onBackClick()
fun onImportPhraseClick()
}
data class Params(
val callbacks: ModelCallbacks,
)
}

View file

@ -1,206 +0,0 @@
package com.tangem.features.hotwallet.addexistingwallet.start
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic.SignedIn
import com.tangem.core.analytics.models.Basic.SignedIn.SignInType
import com.tangem.core.decompose.di.GlobalUiMessageSender
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.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.message.DialogMessage
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.analytics.Shop
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.SaveWalletError
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
private const val HIDE_PROGRESS_DELAY = 400L
@Suppress("LongParameterList")
@ModelScoped
internal class AddExistingWalletStartModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val saveWalletUseCase: SaveWalletUseCase,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val scanCardProcessor: ScanCardProcessor,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appRouter: AppRouter,
private val urlOpener: UrlOpener,
private val userWalletsListRepository: UserWalletsListRepository,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() {
private val params: AddExistingWalletStartComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<AddExistingWalletStartUM>
field = MutableStateFlow(
AddExistingWalletStartUM(
showWantToPurchaseBlock = false,
isScanInProgress = false,
onBackClick = params.callbacks::onBackClick,
onImportPhraseClick = params.callbacks::onImportPhraseClick,
onScanCardClick = ::onScanClick,
onBuyCardClick = ::onShopClick,
),
)
init {
showWantToPurchaseBlockWithDelay()
}
private fun showWantToPurchaseBlockWithDelay() {
modelScope.launch {
delay(SHOW_WANT_TO_PURCHASE_BLOCK_DELAY)
uiState.update { it.copy(showWantToPurchaseBlock = true) }
}
}
private fun onShopClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
analyticsEventHandler.send(Shop.ScreenOpened)
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
}
}
private fun onScanClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard)
scanCard()
}
private fun scanCard() {
modelScope.launch {
setLoading(true)
val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes()
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes,
)
val analyticsSource = AnalyticsParam.ScreensSources.Intro
scanCardProcessor.scan(
analyticsSource = analyticsSource,
onProgressStateChange = { showProgress ->
if (!showProgress) {
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
} else {
setLoading(true)
}
},
onFailure = { error ->
handleScanError(error)
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
},
onSuccess = { scanResponse ->
proceedWithScanResponse(scanResponse)
},
)
}
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")
setLoading(false)
return
}
saveWalletUseCase(userWallet).fold(
ifLeft = {
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
when (it) {
is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet")
is SaveWalletError.WalletAlreadySaved -> {
userWalletsListRepository.unlock(
userWalletId = userWallet.walletId,
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse),
).onRight {
appRouter.replaceAll(AppRoute.Wallet)
}
}
}
},
ifRight = {
setLoading(false)
sendSignedInCardAnalyticsEvent(scanResponse)
appRouter.replaceAll(AppRoute.Wallet)
},
)
}
private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
if (currency != null) {
analyticsEventHandler.send(
SignedIn(
currency = currency,
batch = scanResponse.card.batchId,
signInType = SignInType.Card,
walletsCount = userWalletsListRepository.userWalletsSync().size.toString(),
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
}
}
private fun setLoading(isLoading: Boolean) {
uiState.update { it.copy(isScanInProgress = isLoading) }
}
fun handleScanError(error: TangemError) {
when (error) {
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
is TangemSdkError -> Timber.e(error, "Scan error occurred")
else -> Timber.e(error, "Error happened")
}
}
private fun handleNfcFeatureUnavailable() {
uiMessageSender.send(
message = DialogMessage(
message = resourceReference(R.string.nfc_error_unavailable),
title = resourceReference(id = R.string.common_error),
),
)
}
companion object {
private const val SHOW_WANT_TO_PURCHASE_BLOCK_DELAY = 3000L
}
}

View file

@ -1,20 +0,0 @@
package com.tangem.features.hotwallet.addexistingwallet.start.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface AddExistingWalletStartModule {
@Binds
@IntoMap
@ClassKey(AddExistingWalletStartModel::class)
fun bindAddExistingWalletStartModel(model: AddExistingWalletStartModel): Model
}

View file

@ -1,10 +0,0 @@
package com.tangem.features.hotwallet.addexistingwallet.start.entity
internal data class AddExistingWalletStartUM(
val showWantToPurchaseBlock: Boolean,
val isScanInProgress: Boolean,
val onBackClick: () -> Unit,
val onImportPhraseClick: () -> Unit,
val onScanCardClick: () -> Unit,
val onBuyCardClick: () -> Unit,
)

View file

@ -1,191 +0,0 @@
package com.tangem.features.hotwallet.addexistingwallet.start.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
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.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
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.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM
import com.tangem.features.hotwallet.common.ui.OptionBlock
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize()
.systemBarsPadding(),
) {
TangemTopAppBar(
modifier = Modifier
.statusBarsPadding(),
startButton = TopAppBarButtonUM.Back(state.onBackClick),
title = "Add existing wallet",
)
Column(
modifier = Modifier
.weight(1f)
.padding(
start = 16.dp,
top = 24.dp,
end = 16.dp,
),
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
text = stringResourceSafe(R.string.wallet_import_seed_navtitle),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
OptionBlock(
modifier = Modifier
.padding(top = 32.dp),
backgroundColor = TangemTheme.colors.background.secondary,
title = stringResourceSafe(R.string.wallet_import_seed_title),
description = stringResourceSafe(R.string.wallet_import_seed_description),
badge = null,
onClick = state.onImportPhraseClick,
enabled = true,
)
OptionBlock(
modifier = Modifier
.padding(top = 8.dp),
backgroundColor = TangemTheme.colors.background.secondary,
title = stringResourceSafe(R.string.wallet_import_scan_title),
description = stringResourceSafe(R.string.wallet_import_scan_description),
badge = {
if (state.isScanInProgress) {
CircularProgressIndicator(
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
.padding(2.dp),
color = TangemTheme.colors.text.primary1,
strokeWidth = TangemTheme.dimens.size2,
)
} else {
Icon(
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp),
painter = painterResource(R.drawable.ic_tangem_24),
contentDescription = null,
tint = TangemTheme.colors.icon.secondary,
)
}
},
onClick = state.onScanCardClick,
enabled = true,
)
OptionBlock(
modifier = Modifier
.padding(top = 8.dp),
backgroundColor = TangemTheme.colors.background.secondary,
title = stringResourceSafe(R.string.wallet_import_google_drive_title),
description = stringResourceSafe(R.string.wallet_import_google_drive_description),
badge = {
Box(
modifier = Modifier
.padding(horizontal = 4.dp)
.background(
color = TangemTheme.colors.field.focused,
shape = TangemTheme.shapes.roundedCorners8,
)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
Text(
text = stringResourceSafe(R.string.common_coming_soon),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
)
}
},
onClick = null,
enabled = false,
)
}
AnimatedVisibility(state.showWantToPurchaseBlock) {
BuyTangemWalletBlock(
onScanClick = state.onBuyCardClick,
)
}
}
}
@Composable
private fun BuyTangemWalletBlock(onScanClick: () -> Unit, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
.background(
color = TangemTheme.colors.field.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
horizontal = 20.dp,
vertical = 16.dp,
),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.weight(1f)
.padding(end = 16.dp),
text = stringResourceSafe(R.string.wallet_import_buy_question),
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
PrimaryButton(
modifier = Modifier
.wrapContentWidth(),
text = stringResourceSafe(R.string.wallet_import_buy_title),
onClick = onScanClick,
size = TangemButtonSize.RoundedAction,
colors = TangemButtonsDefaults.secondaryButtonColors,
enabled = true,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewCreateWalletContent() {
TangemThemePreview {
AddExistingWalletStartContent(
state = AddExistingWalletStartUM(
showWantToPurchaseBlock = true,
isScanInProgress = true,
onBackClick = {},
onImportPhraseClick = {},
onScanCardClick = {},
onBuyCardClick = {},
),
)
}
}

View file

@ -31,11 +31,16 @@ internal class CreateMobileWalletModel @Inject constructor(
field = MutableStateFlow(
CreateMobileWalletUM(
onBackClick = { router.pop() },
onImportClick = ::onImportClick,
onCreateClick = ::onCreateClick,
createButtonLoading = false,
),
)
private fun onImportClick() {
router.push(AppRoute.AddExistingWallet)
}
private fun onCreateClick() {
modelScope.launch {
uiState.update {

View file

@ -3,5 +3,6 @@ package com.tangem.features.hotwallet.createmobilewallet.entity
internal data class CreateMobileWalletUM(
val createButtonLoading: Boolean,
val onBackClick: () -> Unit,
val onImportClick: () -> Unit,
val onCreateClick: () -> Unit,
)

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.TextReference
@ -80,13 +81,23 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo
iconRes = R.drawable.ic_settings_24,
)
}
SecondaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 16.dp,
top = 16.dp,
end = 16.dp,
),
text = stringResourceSafe(R.string.hw_import_existing_wallet),
onClick = state.onImportClick,
)
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = stringResourceSafe(R.string.common_create),
text = stringResourceSafe(R.string.onboarding_create_wallet_button_create_wallet),
showProgress = state.createButtonLoading,
enabled = true,
onClick = state.onCreateClick,
)
}
@ -134,6 +145,7 @@ private fun PreviewCreateWalletContent() {
state = CreateMobileWalletUM(
onBackClick = {},
createButtonLoading = false,
onImportClick = {},
onCreateClick = {},
),
)