From a82d8cdbbad4b4faa540a7351f27c977e9f8288d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 May 2026 11:27:43 -0700 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../AppsFlyerReferralParamsHandler.kt | 18 ++- .../component/impl/DefaultRoutingComponent.kt | 21 +++ .../tangem/tap/routing/utils/ChildFactory.kt | 20 ++- .../com/tangem/common/routing/AppRoute.kt | 12 +- .../core/analytics/models/AnalyticsParam.kt | 1 + .../configs/feature_toggles_config.json | 4 + .../local/appsflyer/AppsFlyerStore.kt | 15 ++ .../local/appsflyer/DefaultAppsFlyerStore.kt | 16 +- .../drawable/img_hot_wallet_onboarding.webp | Bin 0 -> 33054 bytes data/appsflyer/build.gradle.kts | 20 +++ .../appsflyer/DefaultAppsFlyerRepository.kt | 20 +++ .../data/appsflyer/di/AppsFlyerDataModule.kt | 30 ++++ .../tangem/data/pay/di/TangemPayDataModule.kt | 5 +- domain/appsflyer/build.gradle.kts | 13 ++ .../appsflyer/AppsFlyerDeeplinkSource.kt | 5 + .../repository/AppsFlyerRepository.kt | 8 + .../usecase/ClearAppsFlyerDeeplinkUseCase.kt | 12 ++ domain/visa/build.gradle.kts | 1 + .../wallets/usecase/CreateHotWalletUseCase.kt | 34 +++++ features/disclaimer/api/build.gradle.kts | 3 + .../api/components/DisclaimerComponent.kt | 2 + .../disclaimer/impl/model/DisclaimerModel.kt | 5 + .../hotwallet/CreateWalletBackupComponent.kt | 3 +- .../hotwallet/UpdateAccessCodeComponent.kt | 2 + .../CreateWalletBackupModel.kt | 12 +- .../updateaccesscode/UpdateAccessCodeModel.kt | 7 +- .../TangemPayHotWalletOnboardingComponent.kt | 8 + .../TangemPayOnboardingComponent.kt | 4 + .../onboarding/impl/build.gradle.kts | 17 +++ .../di/TangemPayOnboardingFeatureModule.kt | 7 + .../di/TangemPayOnboardingModelsModule.kt | 6 + ...ltTangemPayHotWalletOnboardingComponent.kt | 39 +++++ .../TangemPayHotWalletOnboardingModel.kt | 102 +++++++++++++ .../TangemPayHotWalletOnboardingScreen.kt | 142 ++++++++++++++++++ .../TangemPayHotWalletOnboardingUM.kt | 7 + .../model/TangemPayOnboardingModel.kt | 8 +- .../tangempay/ui/TandemPayOnboardingScreen.kt | 69 +-------- .../ui/TangemPayOnboardingButtons.kt | 42 ++++++ .../ui/TangemPayOnboardingFeatureInfo.kt | 50 ++++++ .../TangemPayHotWalletOnboardingModelTest.kt | 120 +++++++++++++++ .../model/WalletSettingsModel.kt | 5 +- settings.gradle.kts | 2 + 43 files changed, 829 insertions(+), 89 deletions(-) create mode 100644 core/ui/src/main/res/drawable/img_hot_wallet_onboarding.webp create mode 100644 data/appsflyer/build.gradle.kts create mode 100644 data/appsflyer/src/main/java/com/tangem/data/appsflyer/DefaultAppsFlyerRepository.kt create mode 100644 data/appsflyer/src/main/java/com/tangem/data/appsflyer/di/AppsFlyerDataModule.kt create mode 100644 domain/appsflyer/build.gradle.kts create mode 100644 domain/appsflyer/src/main/kotlin/com/tangem/domain/appsflyer/AppsFlyerDeeplinkSource.kt create mode 100644 domain/appsflyer/src/main/kotlin/com/tangem/domain/appsflyer/repository/AppsFlyerRepository.kt create mode 100644 domain/appsflyer/src/main/kotlin/com/tangem/domain/appsflyer/usecase/ClearAppsFlyerDeeplinkUseCase.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/CreateHotWalletUseCase.kt create mode 100644 features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayHotWalletOnboardingComponent.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/DefaultTangemPayHotWalletOnboardingComponent.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModel.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingScreen.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingUM.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingButtons.kt create mode 100644 features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingFeatureInfo.kt create mode 100644 features/tangempay/onboarding/impl/src/test/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModelTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index eef20245e6..26e17d1dc3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -207,6 +207,7 @@ dependencies { implementation(projects.data.txhistory) implementation(projects.data.wallets) implementation(projects.data.analytics) + implementation(projects.data.appsflyer) implementation(projects.data.transaction) implementation(projects.data.visa) implementation(projects.data.stories) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt index 51352ef3ee..8886b70405 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt @@ -1,6 +1,7 @@ package com.tangem.tap.common.analytics.appsflyer import com.appsflyer.deeplink.DeepLink +import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase @@ -40,11 +41,20 @@ class AppsFlyerReferralParamsHandler @Inject constructor( } private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) { - if (deepLinkValue != REFERRAL_DEEP_LINK_VALUE) { - TangemLogger.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}") - return + when (deepLinkValue) { + REFERRAL_DEEP_LINK_VALUE -> handleReferral(deepLinkSub1, deepLinkSub2) + TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE -> handleTangemPayHotWalletOnboarding(deepLinkValue) + else -> TangemLogger.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}") } + } + private fun handleTangemPayHotWalletOnboarding(deepLinkValue: String) { + coroutineScope.launch { + appsFlyerStore.storeDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding, deepLinkValue) + } + } + + private fun handleReferral(deepLinkSub1: String?, deepLinkSub2: String?) { @Suppress("NullableToStringCall") TangemLogger.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2") @@ -80,6 +90,8 @@ class AppsFlyerReferralParamsHandler @Inject constructor( private companion object { const val REFERRAL_DEEP_LINK_VALUE = "referral" + const val TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE = "tpay_mobileonboard" + const val DEEP_LINK_VALUE = "deep_link_value" const val DEEP_LINK_SUB_1 = "deep_link_sub1" const val DEEP_LINK_SUB_2 = "deep_link_sub2" diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 91913162ff..f59be94532 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -19,6 +19,8 @@ import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.analytics.utils.TrackingContextProxy +import com.tangem.core.configtoggle.FeatureToggles +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext @@ -29,6 +31,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource +import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.scan.ScanResponse @@ -83,6 +87,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, + private val appsFlyerStore: AppsFlyerStore, private val trackingContextProxy: TrackingContextProxy, private val scanFailsComponentFactory: ScanFailsComponent.Factory, private val scanFailsRequesterProxy: ScanFailsRequesterProxy, @@ -93,6 +98,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase, private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase, private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, + private val featureTogglesManager: FeatureTogglesManager, ) : RoutingComponent, AppComponentContext by context, SnackbarHandler { @@ -200,6 +206,21 @@ internal class DefaultRoutingComponent @AssistedInject constructor( } private suspend fun navigateForEmptyWallets(): AppRoute { + if (featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING)) { + val tangemPayHotWalletOnboardingDeepLink = appsFlyerStore.getDeeplink( + AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding, + ) + if (tangemPayHotWalletOnboardingDeepLink != null) { + val hotWalletRoute = AppRoute.TangemPayHotWalletOnboarding + val shouldShowTos = !cardRepository.isTangemTOSAccepted() + return if (shouldShowTos) { + AppRoute.Disclaimer(isTosAccepted = false, nextRoute = hotWalletRoute) + } else { + hotWalletRoute + } + } + } + val shouldAskPushPermission = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrNull() ?: return AppRoute.Home(launchMode = launchMode) return if (shouldAskPushPermission) { diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 07f41386e5..d4980da8c2 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -36,6 +36,7 @@ import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendEntryPointComponent import com.tangem.features.staking.api.StakingComponent import com.tangem.features.swap.SwapComponent +import com.tangem.features.tangempay.components.TangemPayHotWalletOnboardingComponent import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.* @@ -109,6 +110,7 @@ internal class ChildFactory @Inject constructor( private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, private val tangemPayDetailsContainerComponentFactory: TangemPayDetailsContainerComponent.Factory, private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory, + private val tangemPayWalletOnboardingComponentFactory: TangemPayHotWalletOnboardingComponent.Factory, private val kycComponentFactory: KycComponent.Factory, private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory, private val feedEntryComponentFactory: FeedEntryComponent.Factory, @@ -131,7 +133,10 @@ internal class ChildFactory @Inject constructor( is AppRoute.Disclaimer -> { createComponentChild( context = context, - params = DisclaimerComponent.Params(route.isTosAccepted), + params = DisclaimerComponent.Params( + isTosAccepted = route.isTosAccepted, + nextRoute = route.nextRoute, + ), componentFactory = disclaimerComponentFactory, ) } @@ -573,9 +578,9 @@ internal class ChildFactory @Inject constructor( params = CreateWalletBackupComponent.Params( userWalletId = route.userWalletId, isUpgradeFlow = route.isUpgradeFlow, - shouldSetAccessCode = route.shouldSetAccessCode, analyticsSource = route.analyticsSource, analyticsAction = route.analyticsAction, + nextScreen = route.nextScreen, ), componentFactory = createWalletBackupComponentFactory, ) @@ -586,6 +591,7 @@ internal class ChildFactory @Inject constructor( params = UpdateAccessCodeComponent.Params( userWalletId = route.userWalletId, source = route.source, + nextScreen = route.nextScreen, ), componentFactory = updateAccessCodeComponentFactory, ) @@ -668,6 +674,9 @@ internal class ChildFactory @Inject constructor( is AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding -> ContinueOnboarding( userWalletId = mode.userWalletId, ) + is AppRoute.TangemPayOnboarding.Mode.FirstSetup -> HotWalletOnboarding( + userWalletId = mode.userWalletId, + ) is AppRoute.TangemPayOnboarding.Mode.Deeplink -> Deeplink( deeplink = mode.deeplink, ) @@ -677,6 +686,13 @@ internal class ChildFactory @Inject constructor( componentFactory = tangemPayOnboardingComponentFactory, ) } + is AppRoute.TangemPayHotWalletOnboarding -> { + createComponentChild( + context = context, + params = Unit, + componentFactory = tangemPayWalletOnboardingComponentFactory, + ) + } is AppRoute.Kyc -> { createComponentChild( context = context, diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index d62597e6a8..eff538b261 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -53,6 +53,7 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Disclaimer( val isTosAccepted: Boolean, + val nextRoute: AppRoute? = null, ) : AppRoute(path = "/disclaimer${if (isTosAccepted) "/tos_accepted" else ""}") @Serializable @@ -405,13 +406,14 @@ sealed class AppRoute(val path: String) : Route { val analyticsSource: String, val analyticsAction: String, val isUpgradeFlow: Boolean = false, - val shouldSetAccessCode: Boolean = false, + val nextScreen: AppRoute? = null, ) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}") @Serializable data class UpdateAccessCode( val userWalletId: UserWalletId, val source: String, + val nextScreen: AppRoute? = null, ) : AppRoute(path = "/update_access_code/${userWalletId.stringValue}") @Serializable @@ -457,6 +459,9 @@ sealed class AppRoute(val path: String) : Route { val status: AccountStatus.Payment, ) : AppRoute(path = "/tangem_pay_details/${status.account}") + @Serializable + data object TangemPayHotWalletOnboarding : AppRoute(path = "/tangem_pay_hot_wallet_onboarding") + @Serializable data class TangemPayOnboarding( val mode: Mode, @@ -474,6 +479,11 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, ) : Mode() + @Serializable + data class FirstSetup( + val userWalletId: UserWalletId, + ) : Mode() + @Serializable data object FromBannerOnMain : Mode() diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index e408f3855d..4c03c5b927 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -102,6 +102,7 @@ sealed class AnalyticsParam { Portfolio("Portfolio"), Staking("Staking"), Earn("Earn"), + TangemPayHotWalletOnboarding("TangemPayHotWalletOnboarding"), } sealed class TxSentFrom(val value: String) { diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 009f3f6d17..4adb3415e8 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -82,5 +82,9 @@ { "name": "AND_15009_SWAP_PROVIDER_FILTER_ENABLED", "version": "undefined" + }, + { + "name": "AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING", + "version": "undefined" } ] diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/AppsFlyerStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/AppsFlyerStore.kt index 42ba74a49e..06f57bada6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/AppsFlyerStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/AppsFlyerStore.kt @@ -13,4 +13,19 @@ interface AppsFlyerStore { suspend fun storeIfAbsent(value: AppsFlyerConversionData) suspend fun storeUIDIfAbsent(value: String) + + suspend fun getDeeplink(source: AppsFlyerDeeplinkSource): String? + + suspend fun storeDeeplink(source: AppsFlyerDeeplinkSource, deeplink: String) + + suspend fun clearDeeplink(source: AppsFlyerDeeplinkSource) +} + +enum class AppsFlyerDeeplinkSource { + TangemPayHotWalletOnboarding, + ; + + fun toStoreKey() = when (this) { + TangemPayHotWalletOnboarding -> "tangem_pay_hot_wallet_onboarding" + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/DefaultAppsFlyerStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/DefaultAppsFlyerStore.kt index 8226326f81..5d7fcfb165 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/DefaultAppsFlyerStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/DefaultAppsFlyerStore.kt @@ -56,8 +56,22 @@ internal class DefaultAppsFlyerStore( } } - private companion object { + override suspend fun getDeeplink(source: AppsFlyerDeeplinkSource): String? = + appPreferencesStore.getSyncOrNull(stringPreferencesKey(source.toStoreKey())) + override suspend fun storeDeeplink(source: AppsFlyerDeeplinkSource, deeplink: String) { + appPreferencesStore.editData { preferences -> + preferences[stringPreferencesKey(source.toStoreKey())] = deeplink + } + } + + override suspend fun clearDeeplink(source: AppsFlyerDeeplinkSource) { + appPreferencesStore.editData { preferences -> + preferences.remove(stringPreferencesKey(source.toStoreKey())) + } + } + + private companion object { val UID_KEY = stringPreferencesKey("APPS_FLYER_UID") val CONVERSION_DATA_KEY = stringPreferencesKey("APPS_FLYER_CONVERSION_DATA") } diff --git a/core/ui/src/main/res/drawable/img_hot_wallet_onboarding.webp b/core/ui/src/main/res/drawable/img_hot_wallet_onboarding.webp new file mode 100644 index 0000000000000000000000000000000000000000..4055a337421db865673e5b97d3fad8ebaf25c106 GIT binary patch literal 33054 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

VltSa5U&wLO%M?g*S82v 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 DPVD&C literal 0 HcmV?d00001 diff --git a/data/appsflyer/build.gradle.kts b/data/appsflyer/build.gradle.kts new file mode 100644 index 0000000000..37b32fb365 --- /dev/null +++ b/data/appsflyer/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.data.appsflyer" +} + +dependencies { + implementation(projects.core.datasource) + + implementation(projects.domain.appsflyer) + + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/appsflyer/src/main/java/com/tangem/data/appsflyer/DefaultAppsFlyerRepository.kt b/data/appsflyer/src/main/java/com/tangem/data/appsflyer/DefaultAppsFlyerRepository.kt new file mode 100644 index 0000000000..0e9c5adbcc --- /dev/null +++ b/data/appsflyer/src/main/java/com/tangem/data/appsflyer/DefaultAppsFlyerRepository.kt @@ -0,0 +1,20 @@ +package com.tangem.data.appsflyer + +import com.tangem.datasource.local.appsflyer.AppsFlyerStore +import com.tangem.domain.appsflyer.AppsFlyerDeeplinkSource +import com.tangem.domain.appsflyer.repository.AppsFlyerRepository +import javax.inject.Inject +import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource as StoreDeeplinkSource + +internal class DefaultAppsFlyerRepository @Inject constructor( + private val appsFlyerStore: AppsFlyerStore, +) : AppsFlyerRepository { + + override suspend fun clearDeeplink(source: AppsFlyerDeeplinkSource) { + appsFlyerStore.clearDeeplink(source.toStoreSource()) + } + + private fun AppsFlyerDeeplinkSource.toStoreSource() = when (this) { + AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding -> StoreDeeplinkSource.TangemPayHotWalletOnboarding + } +} \ No newline at end of file diff --git a/data/appsflyer/src/main/java/com/tangem/data/appsflyer/di/AppsFlyerDataModule.kt b/data/appsflyer/src/main/java/com/tangem/data/appsflyer/di/AppsFlyerDataModule.kt new file mode 100644 index 0000000000..27bcc1077f --- /dev/null +++ b/data/appsflyer/src/main/java/com/tangem/data/appsflyer/di/AppsFlyerDataModule.kt @@ -0,0 +1,30 @@ +package com.tangem.data.appsflyer.di + +import com.tangem.data.appsflyer.DefaultAppsFlyerRepository +import com.tangem.domain.appsflyer.repository.AppsFlyerRepository +import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface AppsFlyerDataModule { + + @Binds + @Singleton + fun bindAppsFlyerRepository(repository: DefaultAppsFlyerRepository): AppsFlyerRepository + + companion object { + + @Provides + fun provideClearAppsFlyerDeeplinkUseCase( + appsFlyerRepository: AppsFlyerRepository, + ): ClearAppsFlyerDeeplinkUseCase { + return ClearAppsFlyerDeeplinkUseCase(appsFlyerRepository) + } + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index ade825296a..5a865fd24b 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -26,10 +26,7 @@ import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.flow.PaymentAccountStatusProducer import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.repository.* -import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase -import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase -import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase -import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase +import com.tangem.domain.pay.usecase.* import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase diff --git a/domain/appsflyer/build.gradle.kts b/domain/appsflyer/build.gradle.kts new file mode 100644 index 0000000000..bf0f3c316b --- /dev/null +++ b/domain/appsflyer/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.appsflyer" +} + +dependencies { + implementation(deps.kotlin.coroutines) +} \ No newline at end of file diff --git a/domain/appsflyer/src/main/kotlin/com/tangem/domain/appsflyer/AppsFlyerDeeplinkSource.kt b/domain/appsflyer/src/main/kotlin/com/tangem/domain/appsflyer/AppsFlyerDeeplinkSource.kt new file mode 100644 index 0000000000..07eca402ff --- /dev/null +++ b/domain/appsflyer/src/main/kotlin/com/tangem/domain/appsflyer/AppsFlyerDeeplinkSource.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.appsflyer + +enum class AppsFlyerDeeplinkSource { + TangemPayHotWalletOnboarding, +} \ No newline at end of file diff --git a/domain/appsflyer/src/main/kotlin/com/tangem/domain/appsflyer/repository/AppsFlyerRepository.kt b/domain/appsflyer/src/main/kotlin/com/tangem/domain/appsflyer/repository/AppsFlyerRepository.kt new file mode 100644 index 0000000000..911eba519c --- /dev/null +++ b/domain/appsflyer/src/main/kotlin/com/tangem/domain/appsflyer/repository/AppsFlyerRepository.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.appsflyer.repository + +import com.tangem.domain.appsflyer.AppsFlyerDeeplinkSource + +interface AppsFlyerRepository { + + suspend fun clearDeeplink(source: AppsFlyerDeeplinkSource) +} \ No newline at end of file diff --git a/domain/appsflyer/src/main/kotlin/com/tangem/domain/appsflyer/usecase/ClearAppsFlyerDeeplinkUseCase.kt b/domain/appsflyer/src/main/kotlin/com/tangem/domain/appsflyer/usecase/ClearAppsFlyerDeeplinkUseCase.kt new file mode 100644 index 0000000000..b0ddd7ec25 --- /dev/null +++ b/domain/appsflyer/src/main/kotlin/com/tangem/domain/appsflyer/usecase/ClearAppsFlyerDeeplinkUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.appsflyer.usecase + +import com.tangem.domain.appsflyer.AppsFlyerDeeplinkSource +import com.tangem.domain.appsflyer.repository.AppsFlyerRepository + +class ClearAppsFlyerDeeplinkUseCase( + private val appsFlyerRepository: AppsFlyerRepository, +) { + suspend operator fun invoke(source: AppsFlyerDeeplinkSource) { + appsFlyerRepository.clearDeeplink(source) + } +} \ No newline at end of file diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index ec2d1affc7..1ba3d56862 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.domain.core) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.core.datasource) /** Security */ implementation(deps.spongecastle.core) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/CreateHotWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/CreateHotWalletUseCase.kt new file mode 100644 index 0000000000..7f71437686 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/CreateHotWalletUseCase.kt @@ -0,0 +1,34 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.builder.HotUserWalletBuilder +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.MnemonicType +import com.tangem.utils.coroutines.AppCoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class CreateHotWalletUseCase @Inject constructor( + private val tangemHotSdk: TangemHotSdk, + private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, + private val saveWalletUseCase: SaveWalletUseCase, + private val syncWalletWithRemoteUseCase: SyncWalletWithRemoteUseCase, + private val appCoroutineScope: AppCoroutineScope, +) { + suspend operator fun invoke(auth: HotAuth, mnemonicType: MnemonicType): Either { + return Either.catch { + val hotWalletId = tangemHotSdk.generateWallet(auth, mnemonicType) + val userWallet = hotUserWalletBuilderFactory.create(hotWalletId).build() + + saveWalletUseCase(userWallet) + + appCoroutineScope.launch { + syncWalletWithRemoteUseCase(userWalletId = userWallet.walletId) + } + + userWallet + } + } +} \ No newline at end of file diff --git a/features/disclaimer/api/build.gradle.kts b/features/disclaimer/api/build.gradle.kts index d6fd71c5e8..f1afef8518 100644 --- a/features/disclaimer/api/build.gradle.kts +++ b/features/disclaimer/api/build.gradle.kts @@ -14,6 +14,9 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.ui) + /* Common */ + implementation(projects.common.routing) + /* Compose */ implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/components/DisclaimerComponent.kt b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/components/DisclaimerComponent.kt index df556bb776..9f4e21fda0 100644 --- a/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/components/DisclaimerComponent.kt +++ b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/components/DisclaimerComponent.kt @@ -1,5 +1,6 @@ package com.tangem.features.disclaimer.api.components +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent @@ -8,5 +9,6 @@ interface DisclaimerComponent : ComposableContentComponent { data class Params( val isTosAccepted: Boolean, + val nextRoute: AppRoute? = null, ) } \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt index b8b6d574b4..f1ca63c748 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt @@ -48,6 +48,11 @@ internal class DisclaimerModel @Inject constructor( router.pop() } else { cardRepository.acceptTangemTOS() + val nextRoute = params.nextRoute + if (nextRoute != null) { + router.replaceAll(nextRoute) + return@launch + } val shouldAskPushPermission = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate() if (shouldAskPushPermission) { notificationsRepository.setShouldShowNotifications( diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt index 4ef515352f..0c2bc00e51 100644 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt @@ -1,5 +1,6 @@ package com.tangem.features.hotwallet +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.models.wallet.UserWalletId @@ -9,9 +10,9 @@ interface CreateWalletBackupComponent : ComposableContentComponent { data class Params( val userWalletId: UserWalletId, val isUpgradeFlow: Boolean, - val shouldSetAccessCode: Boolean, val analyticsSource: String, val analyticsAction: String, + val nextScreen: AppRoute? = null, ) interface Factory : ComponentFactory diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpdateAccessCodeComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpdateAccessCodeComponent.kt index a6d223bd14..7c7627342b 100644 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpdateAccessCodeComponent.kt +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpdateAccessCodeComponent.kt @@ -1,5 +1,6 @@ package com.tangem.features.hotwallet +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.models.wallet.UserWalletId @@ -8,6 +9,7 @@ interface UpdateAccessCodeComponent : ComposableContentComponent { data class Params( val userWalletId: UserWalletId, val source: String, + val nextScreen: AppRoute? = null, ) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt index 440dadd85a..75ba090edd 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt @@ -97,19 +97,15 @@ internal class CreateWalletBackupModel @Inject constructor( stackNavigation.push( configuration = CreateWalletBackupRoute.BackupCompleted( isUpgradeFlow = params.isUpgradeFlow, - isLastScreen = !params.shouldSetAccessCode, + isLastScreen = params.nextScreen == null, ), ) } fun onManualBackupCompleted() { - if (params.shouldSetAccessCode) { - router.replaceCurrent( - route = AppRoute.UpdateAccessCode( - userWalletId = params.userWalletId, - source = params.analyticsSource, - ), - ) + val nextScreen = params.nextScreen + if (nextScreen != null) { + router.replaceCurrent(nextScreen) } else { router.pop() } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt index e671521f5d..b822a2ca71 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt @@ -60,7 +60,12 @@ internal class UpdateAccessCodeModel @Inject constructor( inner class MobileWalletSetupFinishedComponentModelCallbacks : MobileWalletSetupFinishedComponent.ModelCallbacks { override fun onFinishClick() { - router.pop() + val nextScreen = params.nextScreen + if (nextScreen != null) { + router.replaceCurrent(nextScreen) + } else { + router.pop() + } } } } \ No newline at end of file diff --git a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayHotWalletOnboardingComponent.kt b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayHotWalletOnboardingComponent.kt new file mode 100644 index 0000000000..6efa50439e --- /dev/null +++ b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayHotWalletOnboardingComponent.kt @@ -0,0 +1,8 @@ +package com.tangem.features.tangempay.components + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface TangemPayHotWalletOnboardingComponent : ComposableContentComponent { + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt index e8c5c6d08d..171d4220c8 100644 --- a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt +++ b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt @@ -16,6 +16,10 @@ interface TangemPayOnboardingComponent : ComposableContentComponent { val userWalletId: UserWalletId, ) : Params() + data class HotWalletOnboarding( + val userWalletId: UserWalletId, + ) : Params() + data object FromBannerOnMain : Params() data object FromBannerInSettings : Params() diff --git a/features/tangempay/onboarding/impl/build.gradle.kts b/features/tangempay/onboarding/impl/build.gradle.kts index 5762f030f4..457040268e 100644 --- a/features/tangempay/onboarding/impl/build.gradle.kts +++ b/features/tangempay/onboarding/impl/build.gradle.kts @@ -11,6 +11,10 @@ android { namespace = "com.tangem.features.tangempay.onboarding.impl" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { /** Core */ implementation(projects.core.analytics) @@ -32,8 +36,14 @@ dependencies { implementation(projects.features.hotWallet.api) /** Domain */ + implementation(projects.domain.appsflyer) implementation(projects.domain.visa) implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + implementation(projects.domain.hotWallet) + + /** Libs */ + implementation(tangemDeps.hot.core) /** Data **/ implementation(projects.data.visa) @@ -52,4 +62,11 @@ dependencies { /** Other */ implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) + + /** Test */ + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) + testImplementation(deps.test.coroutine) } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingFeatureModule.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingFeatureModule.kt index 6e36c8215a..3452722dbd 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingFeatureModule.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingFeatureModule.kt @@ -1,7 +1,9 @@ package com.tangem.features.tangempay.di import com.tangem.features.tangempay.components.DefaultTangemPayOnboardingComponent +import com.tangem.features.tangempay.components.TangemPayHotWalletOnboardingComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent +import com.tangem.features.tangempay.hotwallet.DefaultTangemPayHotWalletOnboardingComponent import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -13,4 +15,9 @@ internal interface TangemPayOnboardingFeatureModule { @Binds fun bindFactory(impl: DefaultTangemPayOnboardingComponent.Factory): TangemPayOnboardingComponent.Factory + + @Binds + fun bindHotWalletOnboardingFactory( + impl: DefaultTangemPayHotWalletOnboardingComponent.Factory, + ): TangemPayHotWalletOnboardingComponent.Factory } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt index 3cd13c821b..0962342cb1 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt @@ -2,6 +2,7 @@ package com.tangem.features.tangempay.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model +import com.tangem.features.tangempay.hotwallet.TangemPayHotWalletOnboardingModel import com.tangem.features.tangempay.model.TangemPayOnboardingModel import com.tangem.features.tangempay.model.TangemPayWalletSelectorModel import dagger.Binds @@ -23,4 +24,9 @@ internal interface TangemPayOnboardingModelsModule { @IntoMap @ClassKey(TangemPayWalletSelectorModel::class) fun bindTangemPayWalletSelectorModel(model: TangemPayWalletSelectorModel): Model + + @Binds + @IntoMap + @ClassKey(TangemPayHotWalletOnboardingModel::class) + fun bindHotWalletOnboardingModel(model: TangemPayHotWalletOnboardingModel): Model } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/DefaultTangemPayHotWalletOnboardingComponent.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/DefaultTangemPayHotWalletOnboardingComponent.kt new file mode 100644 index 0000000000..a9dd09700c --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/DefaultTangemPayHotWalletOnboardingComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.tangempay.hotwallet + +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.components.SystemBarsIconsDisposable +import com.tangem.core.ui.res.ForceDarkTheme +import com.tangem.features.tangempay.components.TangemPayHotWalletOnboardingComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultTangemPayHotWalletOnboardingComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: Unit, +) : TangemPayHotWalletOnboardingComponent, AppComponentContext by context { + + private val model: TangemPayHotWalletOnboardingModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + SystemBarsIconsDisposable(darkIcons = false) + ForceDarkTheme { + TangemPayHotWalletOnboardingScreen( + state = state, + modifier = modifier, + ) + } + } + + @AssistedFactory + interface Factory : TangemPayHotWalletOnboardingComponent.Factory { + override fun create(context: AppComponentContext, params: Unit): DefaultTangemPayHotWalletOnboardingComponent + } +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModel.kt new file mode 100644 index 0000000000..0cfe1a2854 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModel.kt @@ -0,0 +1,102 @@ +package com.tangem.features.tangempay.hotwallet + +import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.models.AnalyticsParam +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.extensions.TextReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.dialog.Dialogs +import com.tangem.domain.appsflyer.AppsFlyerDeeplinkSource +import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase +import com.tangem.domain.hotwallet.IsHotWalletCreationSupported +import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents +import com.tangem.domain.wallets.usecase.CreateHotWalletUseCase +import com.tangem.features.tangempay.TangemPayConstants +import com.tangem.features.tangempay.onboarding.api.R +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.MnemonicType +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@ModelScoped +internal class TangemPayHotWalletOnboardingModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val createHotWalletUseCase: CreateHotWalletUseCase, + private val isHotWalletCreationSupported: IsHotWalletCreationSupported, + private val clearAppsFlyerDeeplinkUseCase: ClearAppsFlyerDeeplinkUseCase, + private val router: Router, + private val uiMessageSender: UiMessageSender, + private val urlOpener: UrlOpener, +) : Model() { + + val uiState: StateFlow + field = MutableStateFlow( + TangemPayHotWalletOnboardingUM( + isLoading = false, + onGetCardClick = ::onGetCardClick, + onTermsClick = ::onTermsClick, + ), + ) + + private fun onTermsClick() { + urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK) + } + + private fun onGetCardClick() { + uiState.update { it.copy(isLoading = true) } + + if (!isHotWalletCreationSupported()) { + uiMessageSender.send( + Dialogs.hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()), + ) + modelScope.launch { clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) } + router.replaceCurrent(AppRoute.Home()) + return + } + + modelScope.launch { + runSuspendCatching { + val userWallet = createHotWalletUseCase.invoke( + auth = HotAuth.NoAuth, + mnemonicType = MnemonicType.Words12, + ).getOrElse { throw it } + + clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) + + router.replaceCurrent( + AppRoute.CreateWalletBackup( + userWalletId = userWallet.walletId, + analyticsSource = AnalyticsParam.ScreensSources.TangemPayHotWalletOnboarding.value, + analyticsAction = WalletSettingsAnalyticEvents.RecoveryPhraseScreenAction.Backup.value, + nextScreen = AppRoute.UpdateAccessCode( + userWalletId = userWallet.walletId, + source = AnalyticsParam.ScreensSources.TangemPayHotWalletOnboarding.value, + nextScreen = AppRoute.TangemPayOnboarding( + mode = AppRoute.TangemPayOnboarding.Mode.FirstSetup(userWallet.walletId), + ), + ), + ), + ) + }.onFailure { + uiState.update { state -> state.copy(isLoading = false) } + uiMessageSender.send( + DialogMessage( + title = TextReference.Res(R.string.common_something_went_wrong), + message = TextReference.Res(R.string.common_unknown_error), + ), + ) + } + } + } +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingScreen.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingScreen.kt new file mode 100644 index 0000000000..7af6c55773 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingScreen.kt @@ -0,0 +1,142 @@ +package com.tangem.features.tangempay.hotwallet + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +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.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton +import com.tangem.core.ui.R +import com.tangem.core.ui.components.TextButton +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.TextReference +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.core.ui.utils.WindowInsetsZero +import com.tangem.features.tangempay.ui.TangemPayOnboardingBlock + +@Composable +internal fun TangemPayHotWalletOnboardingScreen(state: TangemPayHotWalletOnboardingUM, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier, + contentWindowInsets = WindowInsetsZero, + content = { paddingValues -> + Content( + state = state, + modifier = Modifier.padding(paddingValues), + ) + }, + ) +} + +@Composable +private fun Content(state: TangemPayHotWalletOnboardingUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .background( + brush = Brush.linearGradient( + colors = listOf( + TangemTheme.colors.background.primary, + Color.Black, + ), + ), + ) + .systemBarsPadding() + .verticalScroll(rememberScrollState()), + ) { + Text( + modifier = Modifier.padding(40.dp), + text = stringResourceSafe(R.string.tangempay_onboarding_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Image( + modifier = Modifier.fillMaxWidth(), + painter = painterResource(R.drawable.img_hot_wallet_onboarding), + contentDescription = null, + contentScale = ContentScale.FillWidth, + ) + Features(modifier = Modifier.padding(horizontal = 40.dp)) + Spacer(Modifier.weight(1f)) + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + NavigationPrimaryButton( + primaryButton = NavigationButton( + textReference = resourceReference(R.string.tangempay_onboarding_get_card_button_text), + iconRes = R.drawable.ic_tangem_24, + isIconVisible = true, + shouldShowProgress = state.isLoading, + onClick = state.onGetCardClick, + ), + ) + TextButton( + modifier = Modifier.fillMaxWidth(), + text = stringResourceSafe(R.string.tangem_pay_terms_fees_limits), + onClick = state.onTermsClick, + colors = TangemButtonsDefaults.defaultTextButtonColors.copy( + contentColor = TangemTheme.colors.text.primary1, + ), + ) + } + } +} + +@Composable +private fun Features(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + TangemPayOnboardingBlock( + painterRes = R.drawable.ic_mobile_wallet_icon_24, + titleRef = TextReference.Res(R.string.tangempay_onboarding_setup_wallet_title), + descriptionRef = TextReference.Res(R.string.tangempay_onboarding_setup_wallet_description), + ) + TangemPayOnboardingBlock( + painterRes = R.drawable.ic_shopping_basket_24, + titleRef = TextReference.Res(R.string.tangempay_onboarding_purchases_title), + descriptionRef = TextReference.Res(R.string.tangempay_onboarding_purchases_description), + ) + TangemPayOnboardingBlock( + painterRes = R.drawable.ic_credit_card_add_24, + titleRef = TextReference.Res(R.string.tangempay_onboarding_pay_title), + descriptionRef = TextReference.Res(R.string.tangempay_onboarding_pay_description), + ) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + TangemPayHotWalletOnboardingScreen( + state = TangemPayHotWalletOnboardingUM( + isLoading = false, + onGetCardClick = {}, + onTermsClick = {}, + ), + modifier = Modifier.fillMaxSize(), + ) + } +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingUM.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingUM.kt new file mode 100644 index 0000000000..6f51ebab7b --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingUM.kt @@ -0,0 +1,7 @@ +package com.tangem.features.tangempay.hotwallet + +internal data class TangemPayHotWalletOnboardingUM( + val isLoading: Boolean, + val onGetCardClick: () -> Unit, + val onTermsClick: () -> Unit, +) \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index 78a7228a9d..93b9db5768 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -70,6 +70,9 @@ internal class TangemPayOnboardingModel @Inject constructor( is TangemPayOnboardingComponent.Params.ContinueOnboarding -> { openKyc(userWalletId = params.userWalletId) } + is TangemPayOnboardingComponent.Params.HotWalletOnboarding -> { + startOnboarding(userWalletId = params.userWalletId) + } is TangemPayOnboardingComponent.Params.FromBannerInSettings, is TangemPayOnboardingComponent.Params.FromBannerOnMain, -> showOnboarding() @@ -104,7 +107,9 @@ internal class TangemPayOnboardingModel @Inject constructor( } } when (params) { - is TangemPayOnboardingComponent.Params.ContinueOnboarding -> openKyc(userWalletId) + is TangemPayOnboardingComponent.Params.ContinueOnboarding, + is TangemPayOnboardingComponent.Params.HotWalletOnboarding, + -> openKyc(userWalletId) else -> startOnboarding(userWalletId) } } @@ -227,6 +232,7 @@ internal class TangemPayOnboardingModel @Inject constructor( is TangemPayOnboardingComponent.Params.Deeplink, is TangemPayOnboardingComponent.Params.ContinueOnboarding, + is TangemPayOnboardingComponent.Params.HotWalletOnboarding, -> null } } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt index af5e9fcae7..346fea774a 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TandemPayOnboardingScreen.kt @@ -1,7 +1,6 @@ package com.tangem.features.tangempay.ui import android.content.res.Configuration -import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.layout.* @@ -9,7 +8,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -23,14 +21,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp -import com.tangem.common.ui.navigationButtons.NavigationButton -import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.core.ui.R -import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.extensions.TextReference -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 @@ -120,9 +113,10 @@ private fun TangemPayOnboardingContent(state: TangemPayOnboardingScreenState.Con .padding(horizontal = 12.dp), ) } - FooterButtons( + TangemPayOnboardingButtons( modifier = Modifier.padding(bottom = 16.dp), - primaryButtonConfig = state.buttonConfig, + onGetCardClick = state.buttonConfig.onClick, + isLoading = state.buttonConfig.isLoading, onTermsClick = state.onTermsClick, ) } @@ -154,63 +148,6 @@ internal fun TangemPayOnboardingBlocks(modifier: Modifier = Modifier) { } } -@Composable -private fun TangemPayOnboardingBlock( - @DrawableRes painterRes: Int, - titleRef: TextReference, - descriptionRef: TextReference, - modifier: Modifier = Modifier, -) { - Row(modifier = modifier) { - Icon( - painter = painterResource(id = painterRes), - contentDescription = null, - modifier = Modifier.size(width = 24.dp, height = 24.dp), - tint = TangemTheme.colors.icon.accent, - ) - Column( - modifier = Modifier - .padding(start = 12.dp) - .fillMaxWidth(), - ) { - Text( - text = titleRef.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = descriptionRef.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - ) - } - } -} - -@Composable -private fun FooterButtons( - primaryButtonConfig: TangemPayOnboardingScreenState.Content.ButtonConfig, - onTermsClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { - SecondaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe(R.string.tangem_pay_terms_fees_limits), - onClick = onTermsClick, - ) - NavigationPrimaryButton( - primaryButton = NavigationButton( - textReference = resourceReference(R.string.tangempay_onboarding_get_card_button_text), - iconRes = R.drawable.ic_tangem_24, - isIconVisible = true, - shouldShowProgress = primaryButtonConfig.isLoading, - onClick = primaryButtonConfig.onClick, - ), - ) - } -} - @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingButtons.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingButtons.kt new file mode 100644 index 0000000000..f22d09361b --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingButtons.kt @@ -0,0 +1,42 @@ +package com.tangem.features.tangempay.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe + +@Composable +internal fun TangemPayOnboardingButtons( + onGetCardClick: () -> Unit, + isLoading: Boolean, + onTermsClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResourceSafe(R.string.tangem_pay_terms_fees_limits), + onClick = onTermsClick, + ) + NavigationPrimaryButton( + primaryButton = NavigationButton( + textReference = resourceReference(R.string.tangempay_onboarding_get_card_button_text), + iconRes = R.drawable.ic_tangem_24, + isIconVisible = true, + shouldShowProgress = isLoading, + onClick = onGetCardClick, + ), + ) + } +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingFeatureInfo.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingFeatureInfo.kt new file mode 100644 index 0000000000..cb18632c15 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingFeatureInfo.kt @@ -0,0 +1,50 @@ +package com.tangem.features.tangempay.ui + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun TangemPayOnboardingBlock( + @DrawableRes painterRes: Int, + titleRef: TextReference, + descriptionRef: TextReference, + modifier: Modifier = Modifier, +) { + Row(modifier = modifier) { + Icon( + painter = painterResource(id = painterRes), + contentDescription = null, + modifier = Modifier.size(width = 24.dp, height = 24.dp), + tint = TangemTheme.colors.icon.accent, + ) + Column( + modifier = Modifier + .padding(start = 12.dp) + .fillMaxWidth(), + ) { + Text( + text = titleRef.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = descriptionRef.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/test/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModelTest.kt b/features/tangempay/onboarding/impl/src/test/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModelTest.kt new file mode 100644 index 0000000000..2ba50a36c5 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/test/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModelTest.kt @@ -0,0 +1,120 @@ +package com.tangem.features.tangempay.hotwallet + +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth.assertThat +import com.tangem.common.routing.AppRoute +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.message.DialogMessage +import com.tangem.domain.appsflyer.AppsFlyerDeeplinkSource +import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase +import com.tangem.domain.hotwallet.IsHotWalletCreationSupported +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.CreateHotWalletUseCase +import com.tangem.features.tangempay.TangemPayConstants +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.MnemonicType +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +internal class TangemPayHotWalletOnboardingModelTest { + + private val createHotWalletUseCase: CreateHotWalletUseCase = mockk() + private val isHotWalletCreationSupported: IsHotWalletCreationSupported = mockk() { + every { getLeastVersionName() } returns "Android 10" + } + private val clearAppsFlyerDeeplinkUseCase: ClearAppsFlyerDeeplinkUseCase = mockk() + private val router: Router = mockk(relaxed = true) + private val uiMessageSender: UiMessageSender = mockk(relaxed = true) + private val urlOpener: UrlOpener = mockk(relaxed = true) + + private val testUserWalletId = UserWalletId("1234567890ABCDEF") + private val testUserWallet: UserWallet.Hot = mockk(relaxed = true) { + every { walletId } returns testUserWalletId + } + + @Nested + inner class OnTermsClick { + + @Test + fun `WHEN onTermsClick THEN urlOpener called with terms link`() = runTest { + val model = createModel() + + model.uiState.value.onTermsClick.invoke() + + verify { urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK) } + } + } + + @Nested + inner class OnGetCardClick { + + @Test + fun `GIVEN hot wallet creation not supported WHEN onGetCardClick THEN wallet creation not attempted`() = + runTest { + every { isHotWalletCreationSupported() } returns false + coEvery { clearAppsFlyerDeeplinkUseCase(any()) } just Runs + + val model = createModel() + model.uiState.value.onGetCardClick.invoke() + + verify { uiMessageSender.send(any()) } + verify { router.replaceCurrent(AppRoute.Home()) } + coVerify { clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) } + coVerify(exactly = 0) { createHotWalletUseCase(any(), any()) } + } + + @Test + fun `GIVEN hot wallet supported AND wallet creation succeeds WHEN onGetCardClick THEN deeplink cleared AND navigate to CreateWalletBackup`() = + runTest { + every { isHotWalletCreationSupported() } returns true + coEvery { createHotWalletUseCase(HotAuth.NoAuth, MnemonicType.Words12) } returns testUserWallet.right() + coEvery { clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) } just Runs + + val model = createModel() + model.uiState.value.onGetCardClick.invoke() + + coVerify { clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding) } + verify { + router.replaceCurrent( + match { it is AppRoute.CreateWalletBackup && it.userWalletId == testUserWalletId }, + ) + } + } + + @Test + fun `GIVEN hot wallet supported AND wallet creation fails WHEN onGetCardClick THEN error dialog sent`() = + runTest { + every { isHotWalletCreationSupported() } returns true + coEvery { + createHotWalletUseCase(HotAuth.NoAuth, MnemonicType.Words12) + } returns RuntimeException("error").left() + + val model = createModel() + model.uiState.value.onGetCardClick.invoke() + + assertThat(model.uiState.value.isLoading).isFalse() + verify { uiMessageSender.send(match { true }) } + coVerify(exactly = 0) { clearAppsFlyerDeeplinkUseCase(any()) } + verify(exactly = 0) { router.replaceCurrent(any()) } + } + } + + private fun createModel(): TangemPayHotWalletOnboardingModel { + return TangemPayHotWalletOnboardingModel( + dispatchers = TestingCoroutineDispatcherProvider(), + createHotWalletUseCase = createHotWalletUseCase, + isHotWalletCreationSupported = isHotWalletCreationSupported, + clearAppsFlyerDeeplinkUseCase = clearAppsFlyerDeeplinkUseCase, + router = router, + uiMessageSender = uiMessageSender, + urlOpener = urlOpener, + ) + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 2eb12d6343..8f236ca63a 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -432,9 +432,12 @@ internal class WalletSettingsModel @Inject constructor( AppRoute.CreateWalletBackup( userWalletId = params.userWalletId, isUpgradeFlow = false, - shouldSetAccessCode = true, analyticsSource = AnalyticsParam.ScreensSources.WalletSettings.value, analyticsAction = RecoveryPhraseScreenAction.AccessCode.value, + nextScreen = AppRoute.UpdateAccessCode( + userWalletId = params.userWalletId, + source = AnalyticsParam.ScreensSources.WalletSettings.value, + ), ), ) closeBs() diff --git a/settings.gradle.kts b/settings.gradle.kts index e3d58b1f57..a23f949e32 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -361,6 +361,7 @@ include(":domain:push-notification-preferences") include(":domain:transaction") include(":domain:transaction:models") include(":domain:analytics") +include(":domain:appsflyer") include(":domain:visa") include(":domain:visa:models") include(":domain:payment") @@ -423,6 +424,7 @@ include(":data:txhistory") include(":data:wallets") include(":data:analytics") include(":data:transaction") +include(":data:appsflyer") include(":data:visa") include(":data:payment") include(":data:virtual-account")