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 0000000000..4055a33742 Binary files /dev/null and b/core/ui/src/main/res/drawable/img_hot_wallet_onboarding.webp differ 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")