Updated on 2026-08-14
This commit is contained in:
parent
5b516f2c9b
commit
a82d8cdbba
43 changed files with 829 additions and 89 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ sealed class AnalyticsParam {
|
|||
Portfolio("Portfolio"),
|
||||
Staking("Staking"),
|
||||
Earn("Earn"),
|
||||
TangemPayHotWalletOnboarding("TangemPayHotWalletOnboarding"),
|
||||
}
|
||||
|
||||
sealed class TxSentFrom(val value: String) {
|
||||
|
|
|
|||
|
|
@ -82,5 +82,9 @@
|
|||
{
|
||||
"name": "AND_15009_SWAP_PROVIDER_FILTER_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
BIN
core/ui/src/main/res/drawable/img_hot_wallet_onboarding.webp
Normal file
BIN
core/ui/src/main/res/drawable/img_hot_wallet_onboarding.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
20
data/appsflyer/build.gradle.kts
Normal file
20
data/appsflyer/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
13
domain/appsflyer/build.gradle.kts
Normal file
13
domain/appsflyer/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.domain.appsflyer
|
||||
|
||||
enum class AppsFlyerDeeplinkSource {
|
||||
TangemPayHotWalletOnboarding,
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.appsflyer.repository
|
||||
|
||||
import com.tangem.domain.appsflyer.AppsFlyerDeeplinkSource
|
||||
|
||||
interface AppsFlyerRepository {
|
||||
|
||||
suspend fun clearDeeplink(source: AppsFlyerDeeplinkSource)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<Throwable, UserWallet.Hot> {
|
||||
return Either.catch {
|
||||
val hotWalletId = tangemHotSdk.generateWallet(auth, mnemonicType)
|
||||
val userWallet = hotUserWalletBuilderFactory.create(hotWalletId).build()
|
||||
|
||||
saveWalletUseCase(userWallet)
|
||||
|
||||
appCoroutineScope.launch {
|
||||
syncWalletWithRemoteUseCase(userWalletId = userWallet.walletId)
|
||||
}
|
||||
|
||||
userWallet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,9 @@ dependencies {
|
|||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/* Common */
|
||||
implementation(projects.common.routing)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Params, CreateWalletBackupComponent>
|
||||
|
|
|
|||
|
|
@ -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<Params, UpdateAccessCodeComponent>
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,12 @@ internal class UpdateAccessCodeModel @Inject constructor(
|
|||
|
||||
inner class MobileWalletSetupFinishedComponentModelCallbacks : MobileWalletSetupFinishedComponent.ModelCallbacks {
|
||||
override fun onFinishClick() {
|
||||
val nextScreen = params.nextScreen
|
||||
if (nextScreen != null) {
|
||||
router.replaceCurrent(nextScreen)
|
||||
} else {
|
||||
router.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Unit, TangemPayHotWalletOnboardingComponent>
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ android {
|
|||
namespace = "com.tangem.features.tangempay.onboarding.impl"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TangemPayHotWalletOnboardingUM>
|
||||
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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.tangempay.hotwallet
|
||||
|
||||
internal data class TangemPayHotWalletOnboardingUM(
|
||||
val isLoading: Boolean,
|
||||
val onGetCardClick: () -> Unit,
|
||||
val onTermsClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DialogMessage> { 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue