Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-18 14:37:31 +03:00
parent ff1ac5d0e7
commit 283bc49d3a
18 changed files with 247 additions and 314 deletions

View file

@ -30,6 +30,7 @@ import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
@ -170,6 +171,9 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase
@Inject
lateinit var cardRepository: CardRepository
// endregion Injected
override fun onCreate() {
@ -256,6 +260,7 @@ internal class TapApplication : Application(), ImageLoaderFactory {
generalUserWalletsListManager = generalUserWalletsListManager,
wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase,
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
cardRepository = cardRepository,
),
),
)

View file

@ -7,7 +7,6 @@ import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
@ -22,8 +21,7 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
return when (action) {
is GlobalAction.Onboarding.Start -> {
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
val onboardingManager = OnboardingManager(action.scanResponse, usedCardsPrefStorage)
val onboardingManager = OnboardingManager(action.scanResponse)
globalState.copy(onboardingState = OnboardingState(true, onboardingManager))
}
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {

View file

@ -31,9 +31,12 @@ import com.tangem.operations.backup.StartPrimaryCardLinkingTask
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.files.ReadFilesTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.TapSdkError
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
import kotlinx.coroutines.launch
import kotlin.collections.set
@ -185,23 +188,28 @@ private class ScanWalletProcessor(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val activationInProgress = preferencesStorage.usedCardsPrefStorage.isActivationInProgress(card.cardId)
mainScope.launch {
val activationInProgress = store.inject(DaggerGraphState::cardRepository)
.isActivationInProgress(card.cardId)
@Suppress("ComplexCondition")
if (card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty() && activationInProgress) {
StartPrimaryCardLinkingTask().run(session) { linkingResult ->
when (linkingResult) {
is CompletionResult.Success -> {
primaryCard = linkingResult.data
deriveKeysIfNeeded(card, session, callback)
}
is CompletionResult.Failure -> {
deriveKeysIfNeeded(card, session, callback)
@Suppress("ComplexCondition")
if (card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty() &&
activationInProgress
) {
StartPrimaryCardLinkingTask().run(session) { linkingResult ->
when (linkingResult) {
is CompletionResult.Success -> {
primaryCard = linkingResult.data
deriveKeysIfNeeded(card, session, callback)
}
is CompletionResult.Failure -> {
deriveKeysIfNeeded(card, session, callback)
}
}
}
} else {
deriveKeysIfNeeded(card, session, callback)
}
} else {
deriveKeysIfNeeded(card, session, callback)
}
}

View file

@ -29,26 +29,26 @@ import timber.log.Timber
[REDACTED_AUTHOR]
*/
object OnboardingHelper {
fun isOnboardingCase(response: ScanResponse): Boolean {
val cardInfoStorage = preferencesStorage.usedCardsPrefStorage
suspend fun isOnboardingCase(response: ScanResponse): Boolean {
val onboardingManager = store.state.globalState.onboardingState.onboardingManager
val cardId = response.card.cardId
return when {
response.cardTypesResolver.isTangemTwins() -> {
if (!response.twinsIsTwinned()) {
true
} else {
cardInfoStorage.isActivationInProgress(cardId)
onboardingManager?.isActivationInProgress(cardId) ?: false
}
}
response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> {
val emptyWallets = response.card.wallets.isEmpty()
val activationInProgress = cardInfoStorage.isActivationInProgress(cardId)
val activationInProgress = onboardingManager?.isActivationInProgress(cardId)
val backupNotActive = response.card.backupStatus?.isActive != true
emptyWallets || activationInProgress || backupNotActive
emptyWallets || activationInProgress == true || backupNotActive
}
response.card.wallets.isNotEmpty() -> cardInfoStorage.isActivationInProgress(cardId)
response.card.wallets.isNotEmpty() -> onboardingManager?.isActivationInProgress(cardId) ?: false
else -> true
}
}

View file

@ -5,11 +5,12 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.isPositive
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.domain.TapError
@ -17,17 +18,17 @@ import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.domain.model.hasPendingTransactions
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import timber.log.Timber
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class OnboardingManager(
var scanResponse: ScanResponse,
private val usedCardsPrefStorage: UsedCardsPrefStorage,
) {
class OnboardingManager(var scanResponse: ScanResponse) {
private val cardRepository: CardRepository = store.inject(DaggerGraphState::cardRepository)
private var cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null
suspend fun loadArtworkUrl(): String {
@ -76,17 +77,23 @@ class OnboardingManager(
)
}
fun activationStarted(cardId: String) {
usedCardsPrefStorage.activationStarted(cardId)
suspend fun startActivation(cardId: String) {
cardRepository.startCardActivation(cardId)
}
fun activationFinished(cardId: String) {
usedCardsPrefStorage.activationFinished(cardId)
suspend fun finishActivation(cardId: String) {
cardRepository.finishCardActivation(cardId)
}
fun isActivationStarted(cardId: String): Boolean {
return usedCardsPrefStorage.isActivationStarted(cardId)
suspend fun finishActivation(cardIds: List<String>) {
cardRepository.finishCardsActivation(cardIds)
}
suspend fun isActivationStarted(cardId: String): Boolean = cardRepository.isActivationStarted(cardId)
suspend fun isActivationFinished(cardId: String): Boolean = cardRepository.isActivationFinished(cardId)
suspend fun isActivationInProgress(cardId: String): Boolean = cardRepository.isActivationInProgress(cardId)
}
data class OnboardingWalletBalance(

View file

@ -20,6 +20,7 @@ import com.tangem.tap.features.demo.DemoHelper
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.mainScope
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -57,8 +58,10 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
when (action) {
is OnboardingNoteAction.Init -> {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
scope.launch {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
}
is OnboardingNoteAction.LoadCardArtwork -> {
@ -86,8 +89,10 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
}
OnboardingNoteStep.Done -> {
Analytics.send(Onboarding.Finished())
onboardingManager.activationFinished(card.cardId)
postUi(DELAY_SDK_DIALOG_CLOSE) { store.dispatch(OnboardingNoteAction.Confetti.Show) }
mainScope.launch {
onboardingManager.finishActivation(card.cardId)
postUi(DELAY_SDK_DIALOG_CLOSE) { store.dispatch(OnboardingNoteAction.Confetti.Show) }
}
}
else -> Unit
}
@ -101,7 +106,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
val updatedResponse = scanResponse.copy(card = result.data.card)
onboardingManager.scanResponse = updatedResponse
onboardingManager.activationStarted(updatedResponse.card.cardId)
onboardingManager.startActivation(updatedResponse.card.cardId)
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.TopUpWallet))
}
is CompletionResult.Failure -> Unit

View file

@ -11,6 +11,7 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.mainScope
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -43,8 +44,10 @@ private fun handleOtherCardsAction(action: Action) {
when (action) {
is OnboardingOtherCardsAction.Init -> {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
scope.launch {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
}
is OnboardingOtherCardsAction.LoadCardArtwork -> {
@ -67,8 +70,10 @@ private fun handleOtherCardsAction(action: Action) {
}
OnboardingOtherCardsStep.Done -> {
Analytics.send(Onboarding.Finished())
onboardingManager.activationFinished(card.cardId)
postUi(200) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) }
mainScope.launch {
onboardingManager.finishActivation(card.cardId)
postUi(200) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) }
}
}
else -> Unit
}
@ -88,7 +93,7 @@ private fun handleOtherCardsAction(action: Action) {
)
val updatedCard = updatedResponse.card
onboardingManager.scanResponse = updatedResponse
onboardingManager.activationStarted(updatedCard.cardId)
onboardingManager.startActivation(updatedCard.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done))

View file

@ -75,51 +75,61 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
fun startCardActivation(cardId: String) {
if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) {
onboardingManager?.activationStarted(cardId)
scope.launch {
onboardingManager?.startActivation(cardId)
}
}
}
fun finishCardActivation() {
if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) {
Analytics.send(Onboarding.Finished())
onboardingManager?.activationFinished(getScanResponse().card.cardId)
twinCardsState.pairCardId?.let { onboardingManager?.activationFinished(it) }
val cardIds = listOfNotNull(getScanResponse().card.cardId, twinCardsState.pairCardId)
if (cardIds.isNotEmpty()) {
mainScope.launch {
onboardingManager?.finishActivation(cardIds)
}
}
}
}
when (action) {
is TwinCardsAction.Init -> {
if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return
mainScope.launch {
if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return@launch
val scanResponse = getScanResponse()
onboardingManager?.apply {
if (!isActivationStarted(scanResponse.card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
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(dispatchAction)
val scanResponse = getScanResponse()
onboardingManager?.apply {
if (!isActivationStarted(scanResponse.card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning))
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
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(dispatchAction)
}
}
CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning))
}
}
}
}

View file

@ -27,6 +27,7 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
@ -72,8 +73,10 @@ private fun handleWalletAction(action: Action) {
when (action) {
OnboardingWalletAction.Init -> {
ifNotNull(onboardingManager, card) { manager, notNullCard ->
if (!manager.isActivationStarted(notNullCard.cardId)) {
Analytics.send(Onboarding.Started())
mainScope.launch {
if (!manager.isActivationStarted(notNullCard.cardId)) {
Analytics.send(Onboarding.Started())
}
}
}
when {
@ -144,8 +147,10 @@ private fun handleWalletAction(action: Action) {
onboardingManager.scanResponse = updatedResponse
store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false))
startCardActivation(updatedResponse)
store.dispatch(OnboardingWalletAction.ResumeBackup)
mainScope.launch {
onboardingManager.startActivation(updatedResponse.card.cardId)
store.dispatchWithMain(OnboardingWalletAction.ResumeBackup)
}
}
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.WalletAlreadyCreated) {
@ -493,11 +498,13 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
backupService.discardSavedBackup()
}
is BackupAction.DiscardSavedBackup -> {
backupService.primaryCardId?.let {
Analytics.send(Onboarding.Finished())
finishCardsActivationForDiscardedUnfinishedBackup(it)
mainScope.launch {
backupService.primaryCardId?.let {
Analytics.send(Onboarding.Finished())
store.state.globalState.onboardingState.onboardingManager?.finishActivation(it)
}
backupService.discardSavedBackup()
}
backupService.discardSavedBackup()
}
is BackupAction.CheckForUnfinishedBackup -> {
if (backupService.hasIncompletedBackup) {
@ -514,17 +521,21 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
is BackupAction.SkipBackup -> {
Analytics.send(Onboarding.Backup.Skipped())
Analytics.send(Onboarding.Finished())
finishCardActivation(gatherCardIds(backupState, card))
scope.launch {
store.state.globalState.onboardingState.onboardingManager?.finishActivation(
cardIds = gatherCardIds(backupState, card),
)
}
}
is BackupAction.FinishBackup -> {
if (action.withAnalytics) {
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
}
scope.launch {
if (action.withAnalytics) {
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
}
if (scanResponse != null) {
scope.launch {
if (scanResponse != null) {
val userWallet = UserWalletBuilder(scanResponse)
.backupCardsIds(backupState.backupCardIds.toSet())
.build()
@ -546,16 +557,22 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
)
store.dispatchOnMain(GlobalAction.UpdateUserWalletsListManager(userWalletsListManager))
}
val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull {
if (store.state.globalState.onboardingState.onboardingManager?.isActivationFinished(it) == true) {
null
} else {
it
}
}
// All cardIds may already be activated if the backup was skipped before.
if (notActivatedCardIds.isEmpty()) return@launch
Analytics.send(Onboarding.Finished())
store.state.globalState.onboardingState.onboardingManager?.finishActivation(notActivatedCardIds)
}
val notActivatedCardIds = gatherCardIds(backupState, card)
.mapNotNull { if (cardActivationIsFinished(it)) null else it }
// All cardIds may already be activated if the backup was skipped before.
if (notActivatedCardIds.isEmpty()) return
Analytics.send(Onboarding.Finished())
finishCardActivation(notActivatedCardIds)
}
is BackupAction.ResetBackupCard -> {
@ -566,36 +583,12 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
private fun cardActivationIsFinished(cardId: String): Boolean {
return preferencesStorage.usedCardsPrefStorage.isActivationFinished(cardId)
}
/**
* Standard Wallet cards start activation at OnboardingWalletAction.CreateWallet
*/
private fun startCardActivation(scanResponse: ScanResponse) {
preferencesStorage.usedCardsPrefStorage.activationStarted(scanResponse.card.cardId)
}
/**
* Standard Wallet cards finish activation at BackupAction.SkipBackup and BackupAction.FinishBackup
*/
internal fun finishCardActivation(cardIds: List<String>) {
cardIds.forEach { cardId ->
preferencesStorage.usedCardsPrefStorage.activationFinished(cardId)
}
}
internal fun gatherCardIds(backupState: BackupState, card: CardDTO?): List<String> {
return (listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds)
.filterNotNull()
.distinct()
}
private fun finishCardsActivationForDiscardedUnfinishedBackup(cardId: String) {
preferencesStorage.usedCardsPrefStorage.activationFinished(cardId)
}
private fun handleOnBackPressed(state: OnboardingWalletState) {
when (state.backupState.backupStep) {
BackupStep.InitBackup, BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode,

View file

@ -8,6 +8,7 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository
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.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
@ -63,6 +64,7 @@ data class DaggerGraphState(
val generalUserWalletsListManager: UserWalletsListManager? = null,
val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null,
val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null,
val cardRepository: CardRepository? = null,
) : StateType {
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {

View file

@ -21,7 +21,7 @@ internal object AppPreferencesStoreModule {
fun provideAppPreferencesStore(
@ApplicationContext appContext: Context,
dispatchers: CoroutineDispatcherProvider,
@NetworkMoshi moshi: Moshi,
@SdkMoshi moshi: Moshi,
): AppPreferencesStore {
return AppPreferencesStore(
preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io),

View file

@ -45,7 +45,7 @@ class AppPreferencesStore(
* @see getObjectList
* */
inline fun <reified T> MutablePreferences.getObject(key: Preferences.Key<String>): T? {
val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types
val adapter = moshi.adapter(T::class.java)
return this[key]?.let(adapter::fromJson)
}
@ -55,6 +55,15 @@ class AppPreferencesStore(
return this[key]?.let(adapter::fromJson)
}
/** Get list of data [T] by string [key] or default */
inline fun <reified T> MutablePreferences.getObjectListOrDefault(
key: Preferences.Key<String>,
default: List<T>,
): List<T> {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
return this[key]?.let(adapter::fromJson) ?: default
}
/** Get map with [String] key and value [V] by string [key] from [MutablePreferences] */
inline fun <reified V> MutablePreferences.getObjectMap(key: Preferences.Key<String>): Map<String, V>? {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)

View file

@ -1,9 +1,11 @@
package com.tangem.data.card
import androidx.datastore.preferences.core.MutablePreferences
import com.tangem.datasource.local.card.UsedCardInfo
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectList
import com.tangem.datasource.local.preferences.utils.getObjectListSync
import com.tangem.domain.card.repository.CardRepository
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
@ -21,23 +23,73 @@ internal class DefaultCardRepository(
}
override suspend fun setCardWasScanned(cardId: String) {
appPreferencesStore.editData { mutablePreferences ->
val usedCards: List<UsedCardInfo>? = mutablePreferences.getObjectList(
key = PreferencesKeys.USED_CARDS_INFO_KEY,
)
appPreferencesStore.editUsedCards(cardId) { it.copy(isScanned = true) }
}
val updatedUsedCards = usedCards?.updateCard(cardId)
?: listOf(UsedCardInfo(cardId = cardId, isScanned = true))
override suspend fun startCardActivation(cardId: String) {
appPreferencesStore.editUsedCards(cardId) { it.copy(isActivationStarted = true) }
}
mutablePreferences.setObjectList(
key = PreferencesKeys.USED_CARDS_INFO_KEY,
value = updatedUsedCards,
)
override suspend fun finishCardActivation(cardId: String) {
appPreferencesStore.editUsedCards(cardId) {
it.copy(isActivationStarted = true, isActivationFinished = true)
}
}
private fun List<UsedCardInfo>.updateCard(cardId: String): List<UsedCardInfo> {
val card = find { it.cardId == cardId } ?: UsedCardInfo(cardId = cardId, isScanned = true)
return addOrReplace(item = card.copy(isScanned = true), predicate = { it.cardId == cardId })
override suspend fun finishCardsActivation(cardIds: List<String>) {
appPreferencesStore.editData { mutablePreferences ->
val usedCards = mutablePreferences.getUsedCards()
val updatedUsedCards = cardIds.map { cardId ->
usedCards.updateCard(cardId) {
it.copy(isActivationStarted = true, isActivationFinished = true)
}
}
mutablePreferences.setObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards)
}
}
override suspend fun isActivationStarted(cardId: String): Boolean {
return getUsedCardSync(cardId)?.isActivationStarted ?: false
}
override suspend fun isActivationFinished(cardId: String): Boolean {
return getUsedCardSync(cardId)?.isActivationFinished ?: false
}
override suspend fun isActivationInProgress(cardId: String): Boolean {
val card = getUsedCardSync(cardId) ?: return false
return card.isActivationStarted && !card.isActivationFinished
}
private suspend fun AppPreferencesStore.editUsedCards(cardId: String, update: (UsedCardInfo) -> UsedCardInfo) {
editData { mutablePreferences ->
val usedCards = mutablePreferences.getUsedCards()
val updatedUsedCards = usedCards.updateCard(cardId = cardId, update = update)
mutablePreferences.setObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards)
}
}
private fun MutablePreferences.getUsedCards(): List<UsedCardInfo> {
return with(appPreferencesStore) {
getObjectListOrDefault(key = PreferencesKeys.USED_CARDS_INFO_KEY, default = mutableListOf())
}
}
private fun List<UsedCardInfo>.updateCard(
cardId: String,
update: (UsedCardInfo) -> UsedCardInfo,
): List<UsedCardInfo> {
val card = find { it.cardId == cardId } ?: UsedCardInfo(cardId = cardId)
return addOrReplace(item = update(card), predicate = { it.cardId == cardId })
}
private suspend fun getUsedCardSync(cardId: String): UsedCardInfo? {
return appPreferencesStore.getObjectListSync<UsedCardInfo>(PreferencesKeys.USED_CARDS_INFO_KEY)
.firstOrNull { it.cardId == cardId }
}
}

View file

@ -3,10 +3,7 @@ package com.tangem.data.source.preferences
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.data.source.preferences.adapters.BigDecimalAdapter
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
import javax.inject.Inject
// 🔥FIXME: Only logic to work with preferences must be here, must be separated to repositories
@ -14,21 +11,13 @@ import javax.inject.Inject
@Deprecated("Create repository instead")
class PreferencesDataSource @Inject internal constructor(applicationContext: Context) {
val usedCardsPrefStorage: UsedCardsPrefStorage
val disclaimerPrefStorage: DisclaimerPrefStorage
private val preferences: SharedPreferences =
applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
private val moshiConverter = MoshiJsonConverter(
adapters = listOf(BigDecimalAdapter()) + MoshiJsonConverter.getTangemSdkAdapters(),
typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(),
)
init {
incrementLaunchCounter()
usedCardsPrefStorage = UsedCardsPrefStorage(preferences, moshiConverter)
usedCardsPrefStorage.migrate()
disclaimerPrefStorage = DisclaimerPrefStorage(preferences)
}

View file

@ -1,8 +0,0 @@
package com.tangem.data.source.preferences.model
internal data class DataSourceUsedCardInfo(
val cardId: String,
val isScanned: Boolean = false,
val isActivationStarted: Boolean = false,
val isActivationFinished: Boolean = false,
)

View file

@ -1,7 +0,0 @@
package com.tangem.data.source.preferences.model
internal data class DataSourceUsedCardInfoOld(
val cardId: String,
val isScanned: Boolean = false,
val isActivationStarted: Boolean = false,
)

View file

@ -1,150 +0,0 @@
package com.tangem.data.source.preferences.storage
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.data.source.preferences.model.DataSourceUsedCardInfo
import com.tangem.data.source.preferences.model.DataSourceUsedCardInfoOld
/**
[REDACTED_AUTHOR]
*/
@Deprecated("Create repository instead")
class UsedCardsPrefStorage internal constructor(
private val preferences: SharedPreferences,
private val jsonConverter: MoshiJsonConverter,
) {
private val migrationList = mutableListOf(
UserCardInfoToV2(this),
)
internal fun migrate() {
migrationList.forEach { it.migrate() }
migrationList.clear()
}
fun scanned(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isScanned = true)
?: DataSourceUsedCardInfo(cardId, true)
save(foundItem, restoredList)
}
fun wasScanned(cardId: String): Boolean {
return findCardInfo(cardId)?.isScanned ?: false
}
fun activationStarted(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = true)
?: DataSourceUsedCardInfo(cardId, isActivationStarted = true)
save(foundItem, restoredList)
}
fun activationFinished(cardId: String) {
val restoredList = restore()
var foundItem = findCardInfo(cardId, restoredList) ?: DataSourceUsedCardInfo(cardId)
foundItem = foundItem.copy(
isActivationStarted = true,
isActivationFinished = true,
)
save(foundItem, restoredList)
}
fun isActivationStarted(cardId: String): Boolean {
return findCardInfo(cardId)?.isActivationStarted ?: false
}
fun isActivationFinished(cardId: String): Boolean {
return findCardInfo(cardId)?.isActivationFinished ?: false
}
fun isActivationInProgress(cardId: String): Boolean {
val cardInfo = findCardInfo(cardId) ?: return false
return cardInfo.isActivationStarted && !cardInfo.isActivationFinished
}
fun hadFinishedActivation(): Boolean {
return restore().any { it.isActivationFinished }
}
private fun findCardInfo(
cardId: String,
list: MutableList<DataSourceUsedCardInfo>? = null,
): DataSourceUsedCardInfo? {
val findInList = list ?: restore()
return findInList.firstOrNull { it.cardId == cardId }
}
private fun save(usedCardInfo: DataSourceUsedCardInfo?, usedCardsInfo: MutableList<DataSourceUsedCardInfo>) {
val info = usedCardInfo ?: return
with(usedCardsInfo) {
val index = indexOfFirst { it.cardId == info.cardId }
if (index == -1) {
add(info)
} else {
set(index, info)
}
}
save(usedCardsInfo)
}
private fun save(list: MutableList<DataSourceUsedCardInfo>) {
val json = jsonConverter.toJson(list)
preferences.edit { putString(USED_CARDS_INFO_V2, json) }
}
private fun restore(): MutableList<DataSourceUsedCardInfo> {
val json = preferences.getString(USED_CARDS_INFO_V2, null) ?: return mutableListOf()
return try {
jsonConverter.fromJson(json, jsonConverter.typedList(DataSourceUsedCardInfo::class.java))!!
} catch (ex: Exception) {
preferences.edit(true) { remove(USED_CARDS_INFO_V2) }
mutableListOf()
}
}
companion object {
private const val USED_CARDS_INFO_V2 = "usedCardsInfo_v2"
private const val USED_CARDS_INFO = "usedCardsInfo"
}
private class UserCardInfoToV2(
private val storage: UsedCardsPrefStorage,
) : Migration {
override fun migrate() {
val restoredCardsInfo = restore()
if (restoredCardsInfo.isEmpty()) return
val newCardsInfo = restoredCardsInfo.map { cardInfo ->
DataSourceUsedCardInfo(
cardId = cardInfo.cardId,
isScanned = cardInfo.isScanned,
isActivationStarted = true,
isActivationFinished = !cardInfo.isActivationStarted,
)
}.toMutableList()
storage.save(newCardsInfo)
}
private fun restore(): MutableList<DataSourceUsedCardInfoOld> {
val json = storage.preferences.getString(USED_CARDS_INFO, null) ?: return mutableListOf()
return try {
storage.jsonConverter.fromJson(
json,
storage.jsonConverter.typedList(DataSourceUsedCardInfoOld::class.java),
)!!
} catch (ex: Exception) {
mutableListOf()
} finally {
storage.preferences.edit(true) { remove(USED_CARDS_INFO) }
}
}
}
}

View file

@ -7,4 +7,19 @@ interface CardRepository {
fun wasCardScanned(cardId: String): Flow<Boolean>
suspend fun setCardWasScanned(cardId: String)
suspend fun startCardActivation(cardId: String)
suspend fun finishCardActivation(cardId: String)
suspend fun finishCardsActivation(cardIds: List<String>)
@Throws
suspend fun isActivationStarted(cardId: String): Boolean
@Throws
suspend fun isActivationFinished(cardId: String): Boolean
@Throws
suspend fun isActivationInProgress(cardId: String): Boolean
}