Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-18 16:24:15 +05:00
parent 3ac26a45b7
commit 294df69e93
21 changed files with 427 additions and 100 deletions

View file

@ -2,12 +2,12 @@ package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.features.home.impl.analytics.IntroductionProcess
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.demo.DemoHelper

View file

@ -131,6 +131,12 @@ internal object WalletsDomainModule {
)
}
@Provides
@Singleton
fun providesOpenBuyTangemCardUseCase(): GenerateBuyTangemCardLinkUseCase {
return GenerateBuyTangemCardLinkUseCase()
}
@Provides
@Singleton
fun providesGetExploreUrlUseCase(walletsManagersFacade: WalletManagersFacade): GetExploreUrlUseCase {

View file

@ -11,13 +11,13 @@ import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.legacy.unlockIfLockable
import com.tangem.tap.*
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler

View file

@ -1,4 +1,4 @@
package com.tangem.features.home.impl.analytics
package com.tangem.domain.card.analytics
internal sealed class AnalyticsParam {

View file

@ -1,4 +1,4 @@
package com.tangem.features.home.impl.analytics
package com.tangem.domain.card.analytics
import com.tangem.core.analytics.models.AnalyticsEvent

View file

@ -1,18 +1,14 @@
package com.tangem.tap.common.analytics.converters
package com.tangem.domain.card.analytics
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.analytics.models.AnalyticsParam.WalletType
import com.tangem.domain.card.CardTypesResolver
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.utils.converter.Converter
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
/**
[REDACTED_AUTHOR]
*/
class ParamCardCurrencyConverter : Converter<CardTypesResolver, CoreAnalyticsParam.WalletType?> {
class ParamCardCurrencyConverter : Converter<CardTypesResolver, WalletType?> {
override fun convert(value: CardTypesResolver): CoreAnalyticsParam.WalletType? {
if (value.isMultiwalletAllowed()) return CoreAnalyticsParam.WalletType.MultiCurrency
override fun convert(value: CardTypesResolver): WalletType? {
if (value.isMultiwalletAllowed()) return WalletType.MultiCurrency
val type = when {
value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
@ -22,6 +18,6 @@ class ParamCardCurrencyConverter : Converter<CardTypesResolver, CoreAnalyticsPar
else -> null
} ?: return null
return CoreAnalyticsParam.WalletType.SingleCurrency(type.value)
return WalletType.SingleCurrency(type.value)
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.features.home.impl.analytics
package com.tangem.domain.card.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
internal sealed class Shop(
sealed class Shop(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Shop", event, params) {

View file

@ -0,0 +1,7 @@
package com.tangem.domain.core.wallets.error
sealed interface SaveFirstColdWalletError {
data object CreateWalletError : SaveFirstColdWalletError
data class SaveError(val error: SaveWalletError) : SaveFirstColdWalletError
data class SelectError(val error: SelectWalletError) : SaveFirstColdWalletError
}

View file

@ -37,6 +37,11 @@ dependencies {
implementation(tangemDeps.hot.core)
// endregion
/** Other libraries */
implementation(platform(deps.firebase.bom))
implementation(deps.firebase.analytics)
implementation(deps.timber)
// region DI
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)

View file

@ -0,0 +1,23 @@
package com.tangem.domain.wallets.usecase
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class GenerateBuyTangemCardLinkUseCase {
suspend operator fun invoke(): String = suspendCoroutine { cont ->
Firebase.analytics.appInstanceId
.addOnSuccessListener { id ->
cont.resume("$NEW_BUY_WALLET_URL&app_instance_id=$id")
}
.addOnFailureListener {
cont.resume(NEW_BUY_WALLET_URL)
}
}
companion object {
private const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app"
}
}

View file

@ -4,9 +4,9 @@ import arrow.core.Either
import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.CompletionResult
import com.tangem.domain.core.wallets.error.SelectWalletError
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.error.SelectWalletError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.core.wallets.UserWalletsListRepository

View file

@ -18,6 +18,12 @@ dependencies {
/** Hot Wallet Feature */
implementation(projects.features.hotWallet.api)
/** Project - Domain */
implementation(projects.domain.card)
implementation(projects.domain.settings)
implementation(projects.domain.wallets)
implementation(projects.domain.models)
/** Core modules */
implementation(projects.core.configToggles)
implementation(projects.core.analytics)

View file

@ -1,19 +1,63 @@
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.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.core.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.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM
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 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 userWalletsListManager: UserWalletsListManager,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() {
internal val uiState: StateFlow<CreateWalletSelectionUM>
@ -31,10 +75,110 @@ internal class CreateWalletSelectionModel @Inject constructor(
}
private fun onHardwareWalletClick() {
// TODO open card order web page
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
analyticsEventHandler.send(Shop.ScreenOpened)
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
}
}
private fun onScanClick() {
// TODO open card scanning
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 -> appRouter.replaceAll(AppRoute.Wallet)
}
},
ifRight = {
setLoading(false)
sendSignedInCardAnalyticsEvent(scanResponse)
appRouter.replaceAll(AppRoute.Wallet)
},
)
}
private 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 = userWalletsListManager.walletsCount.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),
),
)
}
}

View file

@ -5,10 +5,12 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
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
@ -18,6 +20,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
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.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -179,6 +182,9 @@ private fun AlreadyHaveTangemWalletBlock(
isScanInProgress: Boolean,
modifier: Modifier = Modifier,
) {
var buttonWidth by remember { mutableStateOf(0) }
val density = LocalDensity.current
Row(
modifier = modifier
.fillMaxWidth()
@ -201,9 +207,17 @@ private fun AlreadyHaveTangemWalletBlock(
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
TangemButton(
modifier = Modifier
.wrapContentWidth(),
.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),

View file

@ -51,9 +51,6 @@ dependencies {
implementation(deps.compose.coil)
implementation(deps.decompose.ext.compose)
/** Firebase */
implementation(deps.firebase.analytics)
/** Tangem libraries */
implementation(tangemDeps.card.android)
implementation(tangemDeps.card.core)

View file

@ -1,23 +0,0 @@
package com.tangem.features.home.impl.analytics
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.card.CardTypesResolver
import com.tangem.utils.converter.Converter
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
internal class ParamCardCurrencyConverter : Converter<CardTypesResolver, CoreAnalyticsParam.WalletType?> {
override fun convert(value: CardTypesResolver): CoreAnalyticsParam.WalletType? {
if (value.isMultiwalletAllowed()) return CoreAnalyticsParam.WalletType.MultiCurrency
val type = when {
value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin)
value.getBlockchain() != Blockchain.Unknown -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!)
else -> null
} ?: return null
return CoreAnalyticsParam.WalletType.SingleCurrency(type.value)
}
}

View file

@ -1,7 +1,5 @@
package com.tangem.features.home.impl.model
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRoute
@ -23,20 +21,22 @@ 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.core.wallets.error.SaveWalletError
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.features.home.api.HomeComponent
import com.tangem.features.home.impl.analytics.IntroductionProcess
import com.tangem.features.home.impl.analytics.ParamCardCurrencyConverter
import com.tangem.features.home.impl.analytics.Shop
import com.tangem.features.home.impl.ui.state.HomeUM
import com.tangem.features.home.impl.ui.state.Stories
import com.tangem.features.home.impl.ui.state.getRestrictedStories
@ -64,16 +64,17 @@ internal class HomeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val scanCardProcessor: ScanCardProcessor,
private val saveWalletUseCase: SaveWalletUseCase,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val router: Router,
private val selectWalletUseCase: SelectWalletUseCase,
private val appRouter: AppRouter,
private val getUserCountryUseCase: GetUserCountryUseCase,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val saveWalletUseCase: SaveWalletUseCase,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
private val userWalletsListManager: UserWalletsListManager,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() {
@ -135,10 +136,9 @@ internal class HomeModel @Inject constructor(
private fun onShopClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
analyticsEventHandler.send(Shop.ScreenOpened)
Firebase.analytics.appInstanceId
.addOnSuccessListener { urlOpener.openUrl(url = "$NEW_BUY_WALLET_URL&app_instance_id=$it") }
.addOnFailureListener { urlOpener.openUrl(url = NEW_BUY_WALLET_URL) }
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
}
}
private fun onSearchTokensClick() {
@ -198,24 +198,17 @@ internal class HomeModel @Inject constructor(
saveWalletUseCase(userWallet).fold(
ifLeft = {
Timber.e(it.toString(), "Unable to save user wallet")
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
when (it) {
is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet")
is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet)
}
},
ifRight = {
setLoading(false)
sendSignedInCardAnalyticsEvent(scanResponse)
// Select the wallet using new mechanism
selectWalletUseCase(userWallet.walletId).fold(
ifLeft = {
Timber.e("Unable to select user wallet: $it")
setLoading(false)
},
ifRight = {
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
appRouter.replaceAll(AppRoute.Wallet)
},
)
appRouter.replaceAll(AppRoute.Wallet)
},
)
}
@ -228,7 +221,7 @@ internal class HomeModel @Inject constructor(
currency = currency,
batch = scanResponse.card.batchId,
signInType = SignInType.Card,
walletsCount = "1",
walletsCount = userWalletsListManager.walletsCount.toString(),
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
@ -241,15 +234,9 @@ internal class HomeModel @Inject constructor(
fun handleScanError(error: TangemError) {
when (error) {
is TangemSdkError.NfcFeatureIsUnavailable -> {
handleNfcFeatureUnavailable()
}
is TangemSdkError -> {
Timber.e(error, "Scan error occurred")
}
else -> {
Timber.e(error, "Error happened")
}
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
is TangemSdkError -> Timber.e(error, "Scan error occurred")
else -> Timber.e(error, "Error happened")
}
}
@ -261,8 +248,4 @@ internal class HomeModel @Inject constructor(
),
)
}
companion object {
const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app"
}
}

View file

@ -28,6 +28,7 @@ dependencies {
implementation(projects.core.datasource)
/** Domain */
implementation(projects.domain.card)
implementation(projects.domain.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)

View file

@ -1,18 +1,63 @@
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.core.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.legacy.UserWalletsListManager
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 userWalletsListManager: UserWalletsListManager,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() {
private val params: AddExistingWalletStartComponent.Params = paramsContainer.require()
@ -20,10 +65,119 @@ internal class AddExistingWalletStartModel @Inject constructor(
internal val uiState: StateFlow<AddExistingWalletStartUM>
field = MutableStateFlow(
AddExistingWalletStartUM(
isScanInProgress = false,
onBackClick = params.callbacks::onBackClick,
onImportPhraseClick = params.callbacks::onImportPhraseClick,
onScanCardClick = { /* [REDACTED_TODO_COMMENT] */ },
onBuyCardClick = { /* [REDACTED_TODO_COMMENT] */ },
onScanCardClick = ::onScanClick,
onBuyCardClick = ::onShopClick,
),
)
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 -> appRouter.replaceAll(AppRoute.Wallet)
}
},
ifRight = {
setLoading(false)
sendSignedInCardAnalyticsEvent(scanResponse)
appRouter.replaceAll(AppRoute.Wallet)
},
)
}
private 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 = userWalletsListManager.walletsCount.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),
),
)
}
}

View file

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

View file

@ -3,6 +3,7 @@ package com.tangem.features.hotwallet.addexistingwallet.start.ui
import android.content.res.Configuration
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
@ -74,14 +75,25 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi
title = stringResourceSafe(R.string.wallet_import_scan_title),
description = stringResourceSafe(R.string.wallet_import_scan_description),
badge = {
Icon(
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp),
painter = painterResource(R.drawable.ic_tangem_24),
contentDescription = null,
tint = TangemTheme.colors.icon.secondary,
)
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,
@ -160,6 +172,7 @@ private fun PreviewCreateWalletContent() {
TangemThemePreview {
AddExistingWalletStartContent(
state = AddExistingWalletStartUM(
isScanInProgress = true,
onBackClick = {},
onImportPhraseClick = {},
onScanCardClick = {},