Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-21 14:28:14 +07:00
parent 942b304897
commit a2f57e10be
21 changed files with 257 additions and 33 deletions

View file

@ -60,6 +60,7 @@ dependencies {
implementation(projects.domain.transaction)
implementation(projects.domain.analytics)
implementation(projects.domain.visa)
implementation(projects.domain.onboarding)
implementation(projects.common)
implementation(projects.core.analytics)
@ -88,6 +89,7 @@ dependencies {
implementation(projects.data.transaction)
implementation(projects.data.visa)
implementation(projects.data.promo)
implementation(projects.data.onboarding)
/** Features */
implementation(projects.features.onboarding)

View file

@ -31,6 +31,8 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.LogConfig
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -162,6 +164,12 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var generalUserWalletsListManager: UserWalletsListManager
@Inject
lateinit var wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase
@Inject
lateinit var saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase
// endregion Injected
override fun onCreate() {
@ -246,6 +254,8 @@ internal class TapApplication : Application(), ImageLoaderFactory {
accountCreator = accountCreator,
userWalletsListManagerFeatureToggles = userWalletsListManagerFeatureToggles,
generalUserWalletsListManager = generalUserWalletsListManager,
wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase,
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
),
),
)

View file

@ -0,0 +1,31 @@
package com.tangem.tap.di.domain
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object OnboardingDomainModule {
@Provides
@Singleton
fun provideWasTwinsOnboardingShownUseCase(
onboardingRepository: OnboardingRepository,
): WasTwinsOnboardingShownUseCase {
return WasTwinsOnboardingShownUseCase(onboardingRepository)
}
@Provides
@Singleton
fun provideSaveTwinsOnboardingShownUseCase(
onboardingRepository: OnboardingRepository,
): SaveTwinsOnboardingShownUseCase {
return SaveTwinsOnboardingShownUseCase(onboardingRepository)
}
}

View file

@ -13,12 +13,8 @@ import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.*
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
@ -26,6 +22,10 @@ import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -165,7 +165,10 @@ internal object LegacyScanProcessor {
navigateTo(appScreen) { onProgressStateChange(it) }
} else {
Analytics.setContext(scanResponse)
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync()
if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) {
onWalletNotCreated()
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) }

View file

@ -11,7 +11,6 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.domain.scanCard.chains.*
import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
@ -52,7 +51,7 @@ internal object UseCaseScanProcessor {
add(AnalyticsChain(analyticsEvent))
}
add(DisclaimerChain(store, disclaimerWillShow))
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager, preferencesStorage))
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager))
}
scanCardUseCase(cardId, afterScanChains = chains)

View file

@ -5,7 +5,6 @@ import arrow.core.left
import arrow.core.right
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.util.twinsIsTwinned
@ -13,6 +12,7 @@ import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -20,6 +20,7 @@ import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import org.rekotlin.Store
@ -32,14 +33,12 @@ import org.rekotlin.Store
*
* @param store the [Store] that holds the state of the app.
* @param tapWalletManager manager responsible for handling operations related to the Wallet.
* @param preferencesStore data source for user preferences.
*
* @see Chain for more information about the Chain interface.
*/
class CheckForOnboardingChain(
private val store: Store<AppState>,
private val tapWalletManager: TapWalletManager,
private val preferencesStore: PreferencesDataSource,
) : Chain<ScanCardException.ChainException, ScanResponse> {
override suspend fun invoke(
@ -63,8 +62,12 @@ class CheckForOnboardingChain(
}
else -> {
Analytics.setContext(previousChainResult)
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
.invokeSync()
// If twins was twinned previously but twins welcome not shown
if (previousChainResult.twinsIsTwinned() && !preferencesStore.wasTwinsOnboardingShown()) {
if (previousChainResult.twinsIsTwinned() && !wasTwinsOnboardingShown) {
store.dispatchOnMain(
TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)),
)

View file

@ -26,7 +26,7 @@ import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.preferencesStorage
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
@ -100,15 +100,22 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
if (preferencesStorage.wasTwinsOnboardingShown()) {
val step = when {
!scanResponse.twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet
twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done
else -> TwinCardsStep.TopUpWallet
mainScope.launch {
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
.invokeSync()
val dispatchAction = if (wasTwinsOnboardingShown) {
val step = when {
!scanResponse.twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet
twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done
else -> TwinCardsStep.TopUpWallet
}
TwinCardsAction.SetStepOfScreen(step)
} else {
TwinCardsAction.SetStepOfScreen(TwinCardsStep.Welcome)
}
store.dispatch(TwinCardsAction.SetStepOfScreen(step))
} else {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Welcome))
store.dispatch(dispatchAction)
}
}
CreateTwinWalletMode.RecreateWallet -> {
@ -120,7 +127,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
when (action.step) {
is TwinCardsStep.WelcomeOnly, TwinCardsStep.Welcome -> {
Analytics.send(Onboarding.Twins.ScreenOpened())
preferencesStorage.saveTwinsOnboardingShown()
scope.launch {
store.inject(DaggerGraphState::saveTwinsOnboardingShownUseCase).invoke()
}
}
is TwinCardsStep.CreateFirstWallet -> {
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
@ -295,7 +305,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
CreateTwinWalletMode.RecreateWallet -> {
scope.launch {
val walletsRepository = store.state.daggerGraphState.get(DaggerGraphState::walletsRepository)
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
if (walletsRepository.shouldSaveUserWalletsSync()) {
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)

View file

@ -9,6 +9,8 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -59,6 +61,8 @@ data class DaggerGraphState(
val accountCreator: AccountCreator? = null,
val userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles? = null,
val generalUserWalletsListManager: UserWalletsListManager? = null,
val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null,
val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null,
) : StateType {
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {

View file

@ -7,6 +7,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN
/**
* All preferences keys that DataStore<Preferences> is stored.
@ -54,6 +55,8 @@ object PreferencesKeys {
}
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") }
}
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */
@ -65,6 +68,7 @@ internal fun getTapPrefKeysToMigrate(): Set<String> {
FUNDS_FOUND_DATE_KEY,
USER_WAS_INTERACT_WITH_RATING_KEY,
USED_CARDS_INFO_KEY,
WAS_TWINS_ONBOARDING_SHOWN,
)
.map(Preferences.Key<*>::name)
.toSet()

1
data/onboarding/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,30 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.data.onboarding"
}
dependencies {
// region AndroidX libraries
implementation(deps.androidx.datastore)
// endregion
// region DI
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
// endregion
// region Core modules
implementation(projects.core.datasource)
// endregion
// region Domain modules
implementation(projects.domain.onboarding)
// endregion
}

View file

@ -0,0 +1,33 @@
package com.tangem.data.onboarding
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.onboarding.repository.OnboardingRepository
import kotlinx.coroutines.flow.Flow
/**
* Default implementation of [OnboardingRepository]
*
* @property appPreferencesStore app preferences store
*
[REDACTED_AUTHOR]
*/
internal class DefaultOnboardingRepository(
private val appPreferencesStore: AppPreferencesStore,
) : OnboardingRepository {
override fun wasTwinsOnboardingShown(): Flow<Boolean> {
return appPreferencesStore.get(key = PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN, default = false)
}
override suspend fun wasTwinsOnboardingShownSync(): Boolean {
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN, default = false)
}
override suspend fun saveTwinsOnboardingShown() {
appPreferencesStore.store(key = PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN, value = true)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.data.onboarding.di
import com.tangem.data.onboarding.DefaultOnboardingRepository
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.onboarding.repository.OnboardingRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object OnboardingRepositoryModule {
@Provides
@Singleton
fun provideOnboardingRepository(appPreferencesStore: AppPreferencesStore): OnboardingRepository {
return DefaultOnboardingRepository(appPreferencesStore = appPreferencesStore)
}
}

View file

@ -56,14 +56,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con
putBoolean(OPEN_WELCOME_ON_RESUME_KEY, value)
}
fun saveTwinsOnboardingShown() {
preferences.edit { putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true) }
}
fun wasTwinsOnboardingShown(): Boolean {
return preferences.getBoolean(TWINS_ONBOARDING_SHOWN_KEY, false)
}
private fun incrementLaunchCounter() {
var count = preferences.getInt(APP_LAUNCH_COUNT_KEY, 0)
preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) }
@ -71,7 +63,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con
companion object {
private const val PREFERENCES_NAME = "tapPrefs"
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
private const val SAVE_WALLET_DIALOG_SHOWN_KEY = "saveUserWalletShown"
private const val SAVE_ACCESS_CODES_KEY = "saveAccessCodes"

1
domain/onboarding/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,9 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}
dependencies {
implementation(deps.arrow.core)
implementation(deps.kotlin.coroutines)
}

View file

@ -0,0 +1,19 @@
package com.tangem.domain.onboarding
import com.tangem.domain.onboarding.repository.OnboardingRepository
/**
* Saves that twins onboarding was shown
*
* @property onboardingRepository onboarding repository
*
[REDACTED_AUTHOR]
*/
class SaveTwinsOnboardingShownUseCase(
private val onboardingRepository: OnboardingRepository,
) {
suspend operator fun invoke() {
onboardingRepository.saveTwinsOnboardingShown()
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.domain.onboarding
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.onboarding.repository.OnboardingRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
/**
* Use case for checking if twins onboarding was shown
*
* @property onboardingRepository onboarding repository
*
[REDACTED_AUTHOR]
*/
class WasTwinsOnboardingShownUseCase(
private val onboardingRepository: OnboardingRepository,
) {
/** Get flow with twins onboarding state or error [Throwable] */
operator fun invoke(): Flow<Either<Throwable, Boolean>> {
return onboardingRepository.wasTwinsOnboardingShown()
.map(Boolean::right)
.catch { it.left() }
}
/** Get twins onboarding state synchronously or default value if exception will be thrown */
suspend fun invokeSync(): Boolean {
return runCatching { onboardingRepository.wasTwinsOnboardingShownSync() }.getOrDefault(defaultValue = false)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.onboarding.repository
import kotlinx.coroutines.flow.Flow
/**
* Onboarding repository
*
[REDACTED_AUTHOR]
*/
interface OnboardingRepository {
fun wasTwinsOnboardingShown(): Flow<Boolean>
@Throws
suspend fun wasTwinsOnboardingShownSync(): Boolean
suspend fun saveTwinsOnboardingShown()
}

View file

@ -20,7 +20,7 @@ internal fun BaseExtension.configureCompilerOptions() {
internal fun BaseExtension.configureCompose(project: Project) {
val useCompose = with(project.path) {
contains(":ui") ||
contains(Regex(pattern = ":onboarding\$")) || // TODO: divide on api/impl after migrating all onboarding to module
contains(":features:onboarding") || // TODO: divide on api/impl after migrating all onboarding to module
contains(Regex(pattern = ":presentation\$")) ||
contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA]
contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function

View file

@ -137,6 +137,7 @@ include(":domain:balance-hiding:models")
include(":domain:transaction")
include(":domain:analytics")
include(":domain:visa")
include(":domain:onboarding")
// endregion Domain modules
// region Data modules
@ -154,4 +155,5 @@ include(":data:analytics")
include(":data:transaction")
include(":data:visa")
include(":data:promo")
include(":data:onboarding")
// endregion Data modules