From 294df69e93cf6607e44868c0c7086d4eb63a7bdd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 16:24:15 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../CardContextInterceptor.kt | 4 +- .../tap/di/domain/WalletsDomainModule.kt | 6 + .../welcome/redux/WelcomeMiddleware.kt | 2 +- .../domain/card}/analytics/AnalyticsParam.kt | 2 +- .../card}/analytics/IntroductionProcess.kt | 2 +- .../analytics}/ParamCardCurrencyConverter.kt | 16 +- .../com/tangem/domain/card}/analytics/Shop.kt | 4 +- .../wallets/error/SaveFirstColdWalletError.kt | 7 + domain/wallets/build.gradle.kts | 5 + .../GenerateBuyTangemCardLinkUseCase.kt | 23 +++ .../wallets/usecase/SelectWalletUseCase.kt | 2 +- .../impl/build.gradle.kts | 6 + .../CreateWalletSelectionModel.kt | 148 +++++++++++++++- .../ui/CreateWalletSelectionContent.kt | 18 +- features/home/impl/build.gradle.kts | 3 - .../analytics/ParamCardCurrencyConverter.kt | 23 --- .../features/home/impl/model/HomeModel.kt | 67 +++----- features/hot-wallet/impl/build.gradle.kts | 1 + .../start/AddExistingWalletStartModel.kt | 158 +++++++++++++++++- .../start/entity/AddExistingWalletStartUM.kt | 1 + .../start/ui/AddExistingWalletStartContent.kt | 29 +++- 21 files changed, 427 insertions(+), 100 deletions(-) rename {features/home/impl/src/main/kotlin/com/tangem/features/home/impl => domain/card/src/main/kotlin/com/tangem/domain/card}/analytics/AnalyticsParam.kt (86%) rename {features/home/impl/src/main/kotlin/com/tangem/features/home/impl => domain/card/src/main/kotlin/com/tangem/domain/card}/analytics/IntroductionProcess.kt (91%) rename {app/src/main/java/com/tangem/tap/common/analytics/converters => domain/card/src/main/kotlin/com/tangem/domain/card/analytics}/ParamCardCurrencyConverter.kt (58%) rename {features/home/impl/src/main/kotlin/com/tangem/features/home/impl => domain/card/src/main/kotlin/com/tangem/domain/card}/analytics/Shop.kt (74%) create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt delete mode 100644 features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 8f14501047..805090e6c0 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index eff26615ae..5b21957999 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -131,6 +131,12 @@ internal object WalletsDomainModule { ) } + @Provides + @Singleton + fun providesOpenBuyTangemCardUseCase(): GenerateBuyTangemCardLinkUseCase { + return GenerateBuyTangemCardLinkUseCase() + } + @Provides @Singleton fun providesGetExploreUrlUseCase(walletsManagersFacade: WalletManagersFacade): GetExploreUrlUseCase { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 37690ee484..8a842072d8 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -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 diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt similarity index 86% rename from features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt index def2ff2645..40ce107753 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt @@ -1,4 +1,4 @@ -package com.tangem.features.home.impl.analytics +package com.tangem.domain.card.analytics internal sealed class AnalyticsParam { diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt similarity index 91% rename from features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt index 6115389bfd..0cb4a3685c 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt @@ -1,4 +1,4 @@ -package com.tangem.features.home.impl.analytics +package com.tangem.domain.card.analytics import com.tangem.core.analytics.models.AnalyticsEvent diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt similarity index 58% rename from app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt index c5c7e0ea3c..2398da3650 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt @@ -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 { +class ParamCardCurrencyConverter : Converter { - 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 null } ?: return null - return CoreAnalyticsParam.WalletType.SingleCurrency(type.value) + return WalletType.SingleCurrency(type.value) } } \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt similarity index 74% rename from features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt index 632cc66ef2..62c02a56ab 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt @@ -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 = mapOf(), ) : AnalyticsEvent("Shop", event, params) { diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt new file mode 100644 index 0000000000..24c29fd411 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt @@ -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 +} \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 8fbf7b49dd..a467663bf5 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -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) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt new file mode 100644 index 0000000000..7c80707cb6 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt @@ -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" + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt index e0654f947c..21a6a76755 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt @@ -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 diff --git a/features/create-wallet-selection/impl/build.gradle.kts b/features/create-wallet-selection/impl/build.gradle.kts index 477a6af3ea..da58579502 100644 --- a/features/create-wallet-selection/impl/build.gradle.kts +++ b/features/create-wallet-selection/impl/build.gradle.kts @@ -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) diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt index 242046b568..23d94a6c78 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt @@ -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 @@ -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), + ), + ) } } \ No newline at end of file diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index 0c777f9a97..4106c3d8c6 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -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), diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index 4a4f17d3bf..d06bedde20 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -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) diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt deleted file mode 100644 index 16c104323b..0000000000 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt +++ /dev/null @@ -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 { - - 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) - } -} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index 61614e3a62..70636eda60 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -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" - } } \ No newline at end of file diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index ccc57778cd..2f25fc3478 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -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) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt index 58ea450355..dab15c9946 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt @@ -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 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), + ), + ) + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt index f898f9c1b7..37a5113f35 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt @@ -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, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt index eef14e30c9..6bf693e36b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt @@ -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 = {},