Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-13 13:43:06 +05:00
parent 4bb7fde041
commit 4341673767
20 changed files with 276 additions and 351 deletions

View file

@ -1,72 +1,80 @@
package com.tangem.features.createwalletselection
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.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.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
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.createwalletselection.entity.CreateWalletSelectionUM
import com.tangem.features.createwalletselection.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
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 CreateWalletSelectionModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val scanCardProcessor: ScanCardProcessor,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appRouter: AppRouter,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val saveWalletUseCase: SaveWalletUseCase,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
private val userWalletsListRepository: UserWalletsListRepository,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() {
internal val uiState: StateFlow<CreateWalletSelectionUM>
field = MutableStateFlow(
CreateWalletSelectionUM(
onBackClick = { router.pop() },
onMobileWalletClick = ::onMobileWalletClick,
onHardwareWalletClick = ::onHardwareWalletClick,
onScanClick = ::onScanClick,
blocks = persistentListOf(
CreateWalletSelectionUM.Block(
title = resourceReference(R.string.wallet_create_hardware_title),
titleLabel = LabelUM(
text = resourceReference(R.string.common_recommended),
style = LabelStyle.ACCENT,
),
description = resourceReference(R.string.wallet_add_hardware_description),
features = persistentListOf(
CreateWalletSelectionUM.Feature(
iconResId = R.drawable.ic_add_wallet_16,
title = resourceReference(R.string.wallet_add_hardware_info_create),
),
CreateWalletSelectionUM.Feature(
iconResId = R.drawable.ic_import_seed_16,
title = resourceReference(R.string.wallet_add_import_seed_phrase),
),
),
onClick = ::onHardwareWalletClick,
),
CreateWalletSelectionUM.Block(
title = resourceReference(R.string.wallet_create_mobile_title),
titleLabel = null,
description = resourceReference(R.string.wallet_add_mobile_description),
features = persistentListOf(
CreateWalletSelectionUM.Feature(
iconResId = R.drawable.ic_mobile_wallet_16,
title = resourceReference(R.string.hw_create_title),
),
CreateWalletSelectionUM.Feature(
iconResId = R.drawable.ic_import_seed_16,
title = resourceReference(R.string.wallet_add_import_seed_phrase),
),
),
onClick = ::onMobileWalletClick,
),
),
onBuyClick = ::onBuyClick,
),
)
@ -86,6 +94,10 @@ internal class CreateWalletSelectionModel @Inject constructor(
}
private fun onHardwareWalletClick() {
// TODO [REDACTED_TASK_KEY]
}
private fun onBuyClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
analyticsEventHandler.send(Shop.ScreenOpened)
modelScope.launch {
@ -93,113 +105,6 @@ internal class CreateWalletSelectionModel @Inject constructor(
}
}
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 = 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_ALREADY_HAVE_WALLET_DELAY = 3000L
}

View file

@ -1,11 +1,26 @@
package com.tangem.features.createwalletselection.entity
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
internal data class CreateWalletSelectionUM(
val isScanInProgress: Boolean = false,
val hardwareWalletPrice: String = "$54.90",
val showAlreadyHaveWallet: Boolean = false,
val blocks: ImmutableList<Block>,
val onBackClick: () -> Unit,
val onMobileWalletClick: () -> Unit,
val onHardwareWalletClick: () -> Unit,
val onScanClick: () -> Unit,
)
val onBuyClick: () -> Unit,
) {
data class Block(
val title: TextReference,
val titleLabel: LabelUM?,
val description: TextReference,
val features: ImmutableList<Feature>,
val onClick: () -> Unit,
)
data class Feature(
val iconResId: Int,
val title: TextReference,
)
}

View file

@ -10,23 +10,25 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
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.createwalletselection.entity.CreateWalletSelectionUM
import com.tangem.features.createwalletselection.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@ -34,7 +36,7 @@ import com.tangem.features.createwalletselection.impl.R
internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.background(TangemTheme.colors.background.secondary)
.fillMaxSize()
.systemBarsPadding(),
) {
@ -42,7 +44,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi
modifier = Modifier
.statusBarsPadding(),
colors = TopAppBarDefaults.topAppBarColors(
containerColor = TangemTheme.colors.background.primary,
containerColor = TangemTheme.colors.background.secondary,
),
navigationIcon = {
IconButton(onClick = state.onBackClick) {
@ -58,7 +60,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi
Text(
modifier = Modifier
.padding(16.dp),
text = stringResourceSafe(R.string.wallet_create_nav_info_title),
text = stringResourceSafe(R.string.wallet_add_support_title),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
@ -78,60 +80,33 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
text = stringResourceSafe(R.string.wallet_create_title),
.padding(
start = 16.dp,
end = 16.dp,
bottom = 24.dp,
),
text = stringResourceSafe(R.string.wallet_add_common_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
WalletBlock(
modifier = Modifier
.padding(top = 24.dp),
title = stringResourceSafe(R.string.wallet_create_mobile_title),
description = stringResourceSafe(R.string.wallet_create_mobile_description),
badge = {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.field.focused,
shape = TangemTheme.shapes.roundedCorners8,
)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
Text(
text = stringResourceSafe(R.string.common_free),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.secondary,
)
}
},
onClick = state.onMobileWalletClick,
)
WalletBlock(
title = stringResourceSafe(R.string.wallet_create_hardware_title),
description = stringResourceSafe(R.string.wallet_create_hardware_description),
badge = {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.text.accent.copy(alpha = 0.1f),
shape = TangemTheme.shapes.roundedCorners8,
)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
Text(
text = stringResourceSafe(R.string.wallet_create_hardware_badge, state.hardwareWalletPrice),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.accent,
)
}
},
onClick = state.onHardwareWalletClick,
)
state.blocks.forEach { block ->
WalletBlock(
modifier = Modifier
.padding(top = 8.dp),
title = block.title.resolveReference(),
description = block.description.resolveReference(),
features = block.features,
badge = block.titleLabel?.let {
{ Label(it) }
},
onClick = block.onClick,
)
}
}
AnimatedVisibility(state.showAlreadyHaveWallet) {
AlreadyHaveTangemWalletBlock(
onScanClick = state.onScanClick,
onBuyClick = state.onBuyClick,
isScanInProgress = state.isScanInProgress,
)
}
@ -143,8 +118,9 @@ private fun WalletBlock(
title: String,
description: String,
onClick: () -> Unit,
features: ImmutableList<CreateWalletSelectionUM.Feature>,
modifier: Modifier = Modifier,
badge: @Composable () -> Unit,
badge: @Composable (() -> Unit)? = null,
) {
Column(
modifier = modifier
@ -152,11 +128,14 @@ private fun WalletBlock(
.padding(top = 8.dp)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(
color = TangemTheme.colors.field.primary,
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.clickable(onClick = onClick)
.padding(16.dp),
.padding(
horizontal = 16.dp,
vertical = 12.dp,
),
) {
Row {
Text(
@ -167,7 +146,7 @@ private fun WalletBlock(
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
badge()
badge?.invoke()
}
Text(
modifier = Modifier
@ -176,24 +155,57 @@ private fun WalletBlock(
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
if (features.isNotEmpty()) {
HorizontalDivider(
modifier = Modifier.padding(top = 12.dp),
thickness = 0.5.dp,
color = TangemTheme.colors.stroke.primary,
)
features.forEach {
Feature(
feature = it,
modifier = Modifier
.padding(top = 12.dp),
)
}
}
}
}
@Composable
private fun Feature(feature: CreateWalletSelectionUM.Feature, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = feature.iconResId),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
)
Text(
modifier = Modifier
.weight(1f, fill = false)
.padding(start = 6.dp),
text = feature.title.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
)
}
}
@Composable
private fun AlreadyHaveTangemWalletBlock(
onScanClick: () -> Unit,
onBuyClick: () -> Unit,
isScanInProgress: Boolean,
modifier: Modifier = Modifier,
) {
var buttonWidth by remember { mutableStateOf(0) }
val density = LocalDensity.current
Row(
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
.background(
color = TangemTheme.colors.field.primary,
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
@ -206,30 +218,16 @@ private fun AlreadyHaveTangemWalletBlock(
modifier = Modifier
.weight(1f)
.padding(end = 16.dp),
text = stringResourceSafe(R.string.wallet_create_scan_question),
text = stringResourceSafe(R.string.wallet_add_hardware_purchase),
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
TangemButton(
modifier = Modifier
.conditional(buttonWidth > 0) {
width(with(density) { buttonWidth.toDp() })
}
.onGloballyPositioned { coordinates ->
if (buttonWidth == 0) {
buttonWidth = coordinates.size.width
}
},
text = stringResourceSafe(R.string.wallet_create_scan_title),
onClick = onScanClick,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
SecondaryButton(
text = stringResourceSafe(R.string.wallet_import_buy_title),
onClick = onBuyClick,
size = TangemButtonSize.RoundedAction,
showProgress = isScanInProgress,
colors = TangemButtonsDefaults.secondaryButtonColors,
textStyle = TangemTheme.typography.subtitle1,
enabled = true,
animateContentChange = true,
)
}
}
@ -241,11 +239,45 @@ private fun PreviewCreateWalletContent() {
TangemThemePreview {
CreateWalletSelectionContent(
state = CreateWalletSelectionUM(
showAlreadyHaveWallet = true,
onBackClick = {},
onMobileWalletClick = {},
onHardwareWalletClick = {},
onScanClick = {},
onBackClick = { },
blocks = persistentListOf(
CreateWalletSelectionUM.Block(
title = resourceReference(R.string.wallet_create_hardware_title),
titleLabel = LabelUM(
text = resourceReference(R.string.common_recommended),
style = LabelStyle.ACCENT,
),
description = resourceReference(R.string.wallet_add_hardware_description),
features = persistentListOf(
CreateWalletSelectionUM.Feature(
iconResId = R.drawable.ic_add_wallet_16,
title = resourceReference(R.string.wallet_add_hardware_info_create),
),
CreateWalletSelectionUM.Feature(
iconResId = R.drawable.ic_import_seed_16,
title = resourceReference(R.string.wallet_add_import_seed_phrase),
),
),
onClick = { },
),
CreateWalletSelectionUM.Block(
title = resourceReference(R.string.wallet_create_mobile_title),
titleLabel = null,
description = resourceReference(R.string.wallet_add_mobile_description),
features = persistentListOf(
CreateWalletSelectionUM.Feature(
iconResId = R.drawable.ic_mobile_wallet_16,
title = resourceReference(R.string.hw_create_title),
),
CreateWalletSelectionUM.Feature(
iconResId = R.drawable.ic_import_seed_16,
title = resourceReference(R.string.wallet_add_import_seed_phrase),
),
),
onClick = { },
),
),
onBuyClick = { },
),
)
}