Updated on 2026-08-14

This commit is contained in:
Tangem 2023-04-26 13:20:44 +03:00
parent 938d7baee1
commit 5d1cd85382
30 changed files with 332 additions and 204 deletions

1
data/source/preferences/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,17 @@
plugins {
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.android.library)
id("configuration")
}
dependencies {
implementation(deps.androidx.core.ktx)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// For MoshiJsonConverter
implementation(deps.tangem.card.core)
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.data.source.preferences" />

View file

@ -0,0 +1,73 @@
package com.tangem.data.source.preferences
import android.content.SharedPreferences
import androidx.core.content.edit
import java.util.*
@Deprecated("Create repository instead")
class AppRatingLaunchObserver internal constructor(
private val preferences: SharedPreferences,
private val launchCounts: Int,
) {
private val deferShowing = 20
private val firstShowing = 3
private var fundsFoundDate: Calendar? = null
init {
val msWhenFundsWasFound = preferences.getLong(K_FUNDS_FOUND_DATE, FUNDS_FOUND_DATE_UNDEFINED)
if (msWhenFundsWasFound != FUNDS_FOUND_DATE_UNDEFINED) {
fundsFoundDate = Calendar.getInstance().apply { timeInMillis = msWhenFundsWasFound }
}
}
fun foundWalletWithFunds() {
if (fundsFoundDate != null) return
fundsFoundDate = Calendar.getInstance()
preferences.edit(true) {
putLong(K_FUNDS_FOUND_DATE, fundsFoundDate!!.timeInMillis).apply()
putInt(K_SHOW_RATING_AT_LAUNCH_COUNT, launchCounts + firstShowing)
}
}
fun isReadyToShow(): Boolean {
val fundsDate = fundsFoundDate ?: return false
if (!userWasInteractWithRating()) {
val diff = Calendar.getInstance().timeInMillis - fundsDate.timeInMillis
val diffInDays = diff / DAY_MILLIS
return launchCounts >= getCounterOfNextShowing() && diffInDays >= firstShowing
}
val nextShowing = getCounterOfNextShowing()
return launchCounts >= nextShowing
}
fun applyDelayedShowing() {
updateNextShowing(launchCounts + deferShowing)
}
fun setNeverToShow() {
updateNextShowing(Int.MAX_VALUE)
}
private fun updateNextShowing(at: Int) {
val editor = preferences.edit()
editor.putInt(K_SHOW_RATING_AT_LAUNCH_COUNT, at)
editor.putBoolean(K_USER_WAS_INTERACT_WITH_RATING, true)
editor.apply()
}
private fun userWasInteractWithRating(): Boolean = preferences.getBoolean(K_USER_WAS_INTERACT_WITH_RATING, false)
private fun getCounterOfNextShowing(): Int = preferences.getInt(K_SHOW_RATING_AT_LAUNCH_COUNT, firstShowing)
companion object {
private const val K_SHOW_RATING_AT_LAUNCH_COUNT = "showRatingDialogAtLaunchCount"
private const val K_FUNDS_FOUND_DATE = "fundsFoundDate"
private const val K_USER_WAS_INTERACT_WITH_RATING = "userWasInteractWithRating"
private const val FUNDS_FOUND_DATE_UNDEFINED = -1L
private const val DAY_MILLIS = 1000 * 60 * 60 * 24
}
}

View file

@ -0,0 +1,110 @@
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.adapters.CardBalanceStateAdapter
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
import com.tangem.data.source.preferences.storage.FiatCurrenciesPrefStorage
import com.tangem.data.source.preferences.storage.ToppedUpWalletStorage
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
// TODO: Replace shared preferences with DataStore
@Deprecated("Create repository instead")
class PreferencesDataSource @Inject internal constructor(applicationContext: Context) {
val appRatingLaunchObserver: AppRatingLaunchObserver
val usedCardsPrefStorage: UsedCardsPrefStorage
val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage
val disclaimerPrefStorage: DisclaimerPrefStorage
val toppedUpWalletStorage: ToppedUpWalletStorage
private val preferences: SharedPreferences =
applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
private val moshiConverter = MoshiJsonConverter(
adapters = listOf(BigDecimalAdapter(), CardBalanceStateAdapter()) + MoshiJsonConverter.getTangemSdkAdapters(),
typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(),
)
init {
incrementLaunchCounter()
appRatingLaunchObserver = AppRatingLaunchObserver(preferences, getCountOfLaunches())
usedCardsPrefStorage = UsedCardsPrefStorage(preferences, moshiConverter)
usedCardsPrefStorage.migrate()
fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, moshiConverter)
fiatCurrenciesPrefStorage.migrate()
disclaimerPrefStorage = DisclaimerPrefStorage(preferences)
toppedUpWalletStorage = ToppedUpWalletStorage(preferences, moshiConverter)
}
var zendeskFirstLaunchTime: Long?
get() = preferences.getLong(ZENDESK_FIRST_LAUNCH_KEY, 0).takeIf { it != 0L }
set(value) = preferences.edit { putLong(ZENDESK_FIRST_LAUNCH_KEY, value ?: 0) }
var sprinklrFirstLaunchTime: Long?
get() = preferences.getLong(SPRINKLR_FIRST_LAUNCH_KEY, 0).takeIf { it != 0L }
set(value) = preferences.edit { putLong(SPRINKLR_FIRST_LAUNCH_KEY, value ?: 0) }
var shouldShowSaveUserWalletScreen: Boolean
get() = preferences.getBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, true)
set(value) = preferences.edit {
putBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, value)
}
var shouldSaveUserWallets: Boolean
get() = preferences.getBoolean(SAVE_USER_WALLETS_KEY, false)
set(value) = preferences.edit {
putBoolean(SAVE_USER_WALLETS_KEY, value)
}
var shouldSaveAccessCodes: Boolean
get() = preferences.getBoolean(SAVE_ACCESS_CODES_KEY, false)
set(value) = preferences.edit {
putBoolean(SAVE_ACCESS_CODES_KEY, value)
}
var wasApplicationStopped: Boolean
get() = preferences.getBoolean(APPLICATION_STOPPED_KEY, false)
set(value) = preferences.edit {
putBoolean(APPLICATION_STOPPED_KEY, value)
}
var shouldOpenWelcomeScreenOnResume: Boolean
get() = preferences.getBoolean(OPEN_WELCOME_ON_RESUME_KEY, false)
set(value) = preferences.edit {
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 getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1)
private fun incrementLaunchCounter() {
var count = preferences.getInt(APP_LAUNCH_COUNT_KEY, 0)
preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) }
}
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 ZENDESK_FIRST_LAUNCH_KEY = "chatFirstLaunchKey"
private const val SPRINKLR_FIRST_LAUNCH_KEY = "sprinklrFirstLaunch"
private const val SAVE_WALLET_DIALOG_SHOWN_KEY = "saveUserWalletShown"
private const val SAVE_USER_WALLETS_KEY = "saveUserWallets"
private const val SAVE_ACCESS_CODES_KEY = "saveAccessCodes"
private const val APPLICATION_STOPPED_KEY = "applicationStopped"
private const val OPEN_WELCOME_ON_RESUME_KEY = "openWelcomeOnResume"
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.data.source.preferences.adapters
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import java.math.BigDecimal
class BigDecimalAdapter {
@FromJson
fun fromJson(value: String) = BigDecimal(value)
@ToJson
fun toJson(value: BigDecimal) = value.toString()
}

View file

@ -0,0 +1,20 @@
package com.tangem.data.source.preferences.adapters
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import com.tangem.data.source.preferences.model.DataSourceTopupInfo
class CardBalanceStateAdapter {
@ToJson
fun toJson(src: DataSourceTopupInfo.CardBalanceState): String = src.serializedName
@FromJson
fun fromJson(json: String): DataSourceTopupInfo.CardBalanceState {
return when (json) {
DataSourceTopupInfo.CardBalanceState.Empty.serializedName -> DataSourceTopupInfo.CardBalanceState.Empty
DataSourceTopupInfo.CardBalanceState.Full.serializedName -> DataSourceTopupInfo.CardBalanceState.Full
else -> error("CardBalanceState not found")
}
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.data.source.preferences.di
import android.content.Context
import com.tangem.data.source.preferences.PreferencesDataSource
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object PreferencesStoreModule {
@Provides
@Singleton
fun providePreferencesStore(@ApplicationContext context: Context) = PreferencesDataSource(context)
}

View file

@ -0,0 +1,10 @@
package com.tangem.data.source.preferences.model
data class DataSourceCurrency(
val id: String,
val code: String,
val name: String,
val rateBTC: String,
val unit: String,
val type: String,
)

View file

@ -0,0 +1,7 @@
package com.tangem.data.source.preferences.model
data class DataSourceFiatCurrency(
val code: String,
val name: String,
val symbol: String,
)

View file

@ -0,0 +1,13 @@
package com.tangem.data.source.preferences.model
data class DataSourceTopupInfo(
val walletId: String,
val cardBalanceState: CardBalanceState,
) {
enum class CardBalanceState(val serializedName: String) {
Empty(serializedName = "Empty"),
Full(serializedName = "Full"),
CustomToken(serializedName = "Custom token"),
BlockchainError(serializedName = "Blockchain error"),
}
}

View file

@ -0,0 +1,8 @@
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

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

View file

@ -0,0 +1,21 @@
package com.tangem.data.source.preferences.storage
import android.content.SharedPreferences
import androidx.core.content.edit
/**
[REDACTED_AUTHOR]
*/
@Deprecated("Create repository instead")
class DisclaimerPrefStorage internal constructor(
private val preferences: SharedPreferences,
) {
fun accept(disclaimerKey: String) {
preferences.edit { putBoolean(disclaimerKey, true) }
}
fun isAccepted(disclaimerKey: String): Boolean {
return preferences.getBoolean(disclaimerKey, false)
}
}

View file

@ -0,0 +1,56 @@
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.DataSourceCurrency
import com.tangem.data.source.preferences.model.DataSourceFiatCurrency
/**
[REDACTED_AUTHOR]
*/
@Deprecated("Create repository instead")
class FiatCurrenciesPrefStorage internal constructor(
private val preferences: SharedPreferences,
private val converter: MoshiJsonConverter,
) {
fun migrate() {
preferences.edit(true) {
remove(FIAT_CURRENCIES_KEY_OLD)
remove(APP_CURRENCY_KEY_OLD)
}
}
fun getAppCurrency(): DataSourceFiatCurrency? {
val json = preferences.getString(APP_CURRENCY_KEY, "")
if (json.isNullOrBlank()) return null
return converter.fromJson(json)
}
fun saveAppCurrency(fiatCurrency: DataSourceFiatCurrency) {
val json = converter.toJson(fiatCurrency)
preferences.edit { putString(APP_CURRENCY_KEY, json) }
}
fun save(currencies: List<DataSourceCurrency>) {
val json: String = converter.toJson(currencies)
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
}
fun restore(): List<DataSourceCurrency> {
val json = preferences.getString(FIAT_CURRENCIES_KEY, "")
val type = converter.typedList(DataSourceCurrency::class.java)
if (json.isNullOrBlank()) return emptyList()
return converter.fromJson(json, type) ?: emptyList()
}
companion object {
private const val FIAT_CURRENCIES_KEY_OLD = "fiatCurrencies"
private const val APP_CURRENCY_KEY_OLD = "appCurrency"
private const val FIAT_CURRENCIES_KEY = "fiatCurrencies_v2"
private const val APP_CURRENCY_KEY = "appCurrency_v2"
}
}

View file

@ -0,0 +1,5 @@
package com.tangem.data.source.preferences.storage
internal interface Migration {
fun migrate()
}

View file

@ -0,0 +1,58 @@
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.DataSourceTopupInfo
/**
[REDACTED_AUTHOR]
*/
@Deprecated("Create repository instead")
class ToppedUpWalletStorage internal constructor(
private val preferences: SharedPreferences,
private val jsonConverter: MoshiJsonConverter,
) {
private val walletList: MutableSet<DataSourceTopupInfo> = mutableSetOf()
init {
walletList.addAll(restore())
}
fun save(userWalletInfo: DataSourceTopupInfo): Boolean {
walletList.removeAll { it.walletId == userWalletInfo.walletId }
walletList.add(userWalletInfo)
return save(walletList)
}
fun restore(walletId: String): DataSourceTopupInfo? {
return walletList.firstOrNull { it.walletId == walletId }
}
private fun save(userWallets: MutableSet<DataSourceTopupInfo>): Boolean {
return try {
val json = jsonConverter.toJson(userWallets)
preferences.edit(true) { putString(KEY, json) }
true
} catch (ex: Exception) {
false
}
}
private fun restore(): MutableSet<DataSourceTopupInfo> {
val json = preferences.getString(KEY, null) ?: return mutableSetOf()
return try {
val typedList = jsonConverter.typedList(DataSourceTopupInfo::class.java)
val listData = jsonConverter.fromJson<List<DataSourceTopupInfo>>(json, typedList)!!
listData.toMutableSet()
} catch (ex: Exception) {
preferences.edit(true) { remove(KEY) }
mutableSetOf()
}
}
companion object {
private const val KEY = "userWalletsInfo"
}
}

View file

@ -0,0 +1,146 @@
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
}
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) }
}
}
}
}