Updated on 2026-08-14
This commit is contained in:
commit
285e72a250
404 changed files with 8122 additions and 3606 deletions
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.datasource.api.express
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
|
||||
import com.tangem.datasource.api.express.models.request.PairsRequestBody
|
||||
import com.tangem.datasource.api.express.models.response.*
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Interface of Tangem Express API (new swap mechanism)
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
interface ExpressApi {
|
||||
|
||||
// TODO move first three params to retrofit interceptor
|
||||
@POST("assets")
|
||||
suspend fun getAssets(
|
||||
@Header("api-key") apiKey: String,
|
||||
@Header("user-id") userId: String,
|
||||
@Header("session-id") sessionId: String,
|
||||
@Body body: AssetsRequestBody,
|
||||
): ApiResponse<List<Asset>>
|
||||
|
||||
@POST("pairs")
|
||||
suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse<List<SwapPair>>
|
||||
|
||||
@GET("providers")
|
||||
suspend fun getProviders(): ApiResponse<List<ExchangeProvider>>
|
||||
|
||||
@GET("exchange-quote")
|
||||
suspend fun getExchangeQuote(
|
||||
@Query("fromContractAddress") fromContractAddress: String,
|
||||
@Query("fromNetwork") fromNetwork: String,
|
||||
@Query("toContractAddress") toContractAddress: String,
|
||||
@Query("toNetwork") toNetwork: String,
|
||||
@Query("fromAmount") fromAmount: BigDecimal,
|
||||
@Query("providerId") providerId: Int,
|
||||
@Query("rateType") rateType: RateType,
|
||||
): ApiResponse<ExchangeQuoteResponse>
|
||||
|
||||
@GET("exchange-data")
|
||||
suspend fun getExchangeData(
|
||||
@Query("fromContractAddress") fromContractAddress: String,
|
||||
@Query("fromNetwork") fromNetwork: String,
|
||||
@Query("toContractAddress") toContractAddress: String,
|
||||
@Query("toNetwork") toNetwork: String,
|
||||
@Query("fromAmount") fromAmount: BigDecimal,
|
||||
@Query("providerId") providerId: Int,
|
||||
@Query("rateType") rateType: RateType,
|
||||
@Query("toAddress") toAddress: String,
|
||||
): ApiResponse<ExchangeDataResponse>
|
||||
|
||||
@GET("exchange-results")
|
||||
suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse<ExchangeResultsResponse>
|
||||
}
|
||||
|
|
@ -117,8 +117,8 @@ interface OneInchApi {
|
|||
*/
|
||||
@GET("quote")
|
||||
suspend fun quote(
|
||||
@Query("fromTokenAddress") fromTokenAddress: String,
|
||||
@Query("toTokenAddress") toTokenAddress: String,
|
||||
@Query("src") fromTokenAddress: String,
|
||||
@Query("dst") toTokenAddress: String,
|
||||
@Query("amount") amount: String,
|
||||
@Query("protocols") protocols: String? = null,
|
||||
@Query("fee") fee: String? = null,
|
||||
|
|
@ -128,6 +128,7 @@ interface OneInchApi {
|
|||
@Query("mainRouteParts") mainRouteParts: String? = null,
|
||||
@Query("parts") parts: String? = null,
|
||||
@Query("gasPrice") gasPrice: String? = null,
|
||||
@Query("includeTokensInfo") includeTokensInfo: Boolean = true,
|
||||
): Response<QuoteResponse>
|
||||
|
||||
/**
|
||||
|
|
@ -178,18 +179,18 @@ interface OneInchApi {
|
|||
*/
|
||||
@GET("swap")
|
||||
suspend fun swap(
|
||||
@Query("fromTokenAddress") fromTokenAddress: String,
|
||||
@Query("toTokenAddress") toTokenAddress: String,
|
||||
@Query("src") fromTokenAddress: String,
|
||||
@Query("dst") toTokenAddress: String,
|
||||
@Query("amount") amount: String,
|
||||
@Query("fromAddress") fromAddress: String,
|
||||
@Query("from") fromAddress: String,
|
||||
@Query("slippage") slippage: Int,
|
||||
@Query("protocols") protocols: String? = null,
|
||||
@Query("destReceiver") destinationAddress: String? = null,
|
||||
@Query("referrerAddress") referrerAddress: String? = null,
|
||||
@Query("receiver") destinationAddress: String? = null,
|
||||
@Query("referrer") referrerAddress: String? = null,
|
||||
@Query("fee") fee: String? = null,
|
||||
@Query("disableEstimate") disableEstimate: Boolean? = null,
|
||||
@Query("permit") permit: String? = null,
|
||||
@Query("compatibilityMode") compatibilityMode: Boolean? = null,
|
||||
@Query("compatibility") compatibilityMode: Boolean? = null,
|
||||
@Query("burnChi") burnChi: Boolean? = null,
|
||||
@Query("allowPartialFill") allowPartialFill: Boolean? = null,
|
||||
@Query("parts") parts: String? = null,
|
||||
|
|
@ -198,6 +199,7 @@ interface OneInchApi {
|
|||
@Query("complexityLevel") complexityLevel: String? = null,
|
||||
@Query("gasLimit") gasLimit: String? = null,
|
||||
@Query("gasPrice") gasPrice: String? = null,
|
||||
@Query("includeTokensInfo") includeTokensInfo: Boolean = true,
|
||||
): Response<SwapResponse>
|
||||
//endregion Swap
|
||||
}
|
||||
|
|
@ -5,16 +5,10 @@ import com.squareup.moshi.Json
|
|||
/**
|
||||
* Quote response
|
||||
*
|
||||
* @property fromToken Source token info
|
||||
* @property toToken Destination token info
|
||||
* @property toTokenAmount Expected amount of destination token
|
||||
* @property fromTokenAmount Amount of source token
|
||||
* @property estimatedGas gas fee
|
||||
*/
|
||||
data class QuoteResponse(
|
||||
@Json(name = "fromToken") val fromToken: TokenOneInchDto,
|
||||
@Json(name = "toToken") val toToken: TokenOneInchDto,
|
||||
@Json(name = "toTokenAmount") val toTokenAmount: String,
|
||||
@Json(name = "fromTokenAmount") val fromTokenAmount: String,
|
||||
@Json(name = "estimatedGas") val estimatedGas: Int,
|
||||
@Json(name = "toAmount") val toTokenAmount: String,
|
||||
)
|
||||
|
|
@ -5,7 +5,6 @@ import com.squareup.moshi.Json
|
|||
data class SwapResponse(
|
||||
@Json(name = "fromToken") val fromToken: TokenOneInchDto,
|
||||
@Json(name = "toToken") val toToken: TokenOneInchDto,
|
||||
@Json(name = "toTokenAmount") val toTokenAmount: String,
|
||||
@Json(name = "fromTokenAmount") val fromTokenAmount: String,
|
||||
@Json(name = "toAmount") val toTokenAmount: String,
|
||||
@Json(name = "tx") val transaction: TransactionDto,
|
||||
)
|
||||
|
|
@ -10,10 +10,8 @@ data class QuotesResponse(
|
|||
|
||||
data class Quote(
|
||||
@Json(name = "price")
|
||||
val price: BigDecimal,
|
||||
val price: BigDecimal?,
|
||||
@Json(name = "priceChange24h")
|
||||
val priceChange: BigDecimal,
|
||||
@Json(name = "lastUpdatedAt")
|
||||
val lastUpdated: String,
|
||||
val priceChange: BigDecimal?,
|
||||
)
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
|
|||
appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey,
|
||||
amplitudeApiKey = configValues.amplitudeApiKey,
|
||||
shopify = configValues.shopifyShop,
|
||||
zendesk = configValues.zendesk,
|
||||
sprinklr = configValues.sprinklr,
|
||||
swapReferrerAccount = configValues.swapReferrerAccount,
|
||||
walletConnectProjectId = configValues.walletConnectProjectId,
|
||||
tangemComAuthorization = configValues.tangemComAuthorization,
|
||||
|
|
|
|||
|
|
@ -5,24 +5,9 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
sealed interface ChatConfig
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ZendeskConfig(
|
||||
@Json(name = "zendeskApiKey")
|
||||
val apiKey: String,
|
||||
@Json(name = "zendeskAppId")
|
||||
val appId: String,
|
||||
@Json(name = "zendeskClientId")
|
||||
val clientId: String,
|
||||
@Json(name = "zendeskAccountKey")
|
||||
val accountKey: String,
|
||||
@Json(name = "zendeskUrl")
|
||||
val url: String,
|
||||
) : ChatConfig
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SprinklrConfig(
|
||||
@Json(name = "appID")
|
||||
val appId: String,
|
||||
@Json(name = "baseURL")
|
||||
val baseUrl: String,
|
||||
@Json(name = "appID") val appId: String,
|
||||
@Json(name = "apiKey") val apiKey: String,
|
||||
@Json(name = "environment") val environment: String,
|
||||
) : ChatConfig
|
||||
|
|
@ -15,7 +15,7 @@ data class Config(
|
|||
@Deprecated("Not relevant since version 3.23")
|
||||
val isCreatingTwinCardsAllowed: Boolean = false,
|
||||
val shopify: ShopifyShop? = null,
|
||||
val zendesk: ZendeskConfig? = null,
|
||||
val sprinklr: SprinklrConfig? = null,
|
||||
val swapReferrerAccount: SwapReferrerAccount? = null,
|
||||
val walletConnectProjectId: String = "",
|
||||
val tangemComAuthorization: String? = null,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class ConfigValueModel(
|
|||
val infuraProjectId: String?,
|
||||
val appsFlyer: AppsFlyer,
|
||||
val shopifyShop: ShopifyShop?,
|
||||
val zendesk: ZendeskConfig?,
|
||||
val sprinklr: SprinklrConfig?,
|
||||
val tronGridApiKey: String,
|
||||
val amplitudeApiKey: String,
|
||||
val swapReferrerAccount: SwapReferrerAccount?,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.local.*
|
||||
import com.tangem.datasource.local.preferences.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
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 AppPreferencesStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppPreferencesStore(
|
||||
@ApplicationContext appContext: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
): AppPreferencesStore {
|
||||
return AppPreferencesStore(
|
||||
preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io),
|
||||
moshi = moshi,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ class OneInchApisModule {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val ONE_INCH_BASE_URL = "https://api-tangem.1inch.io/v5.0/"
|
||||
private const val ONE_INCH_BASE_URL = "https://api-tangem.1inch.io/v5.2/"
|
||||
private const val ONE_INCH_ETH_PATH = "1/"
|
||||
private const val ONE_INCH_BSC_PATH = "56/"
|
||||
private const val ONE_INCH_POLYGON_PATH = "137/"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.files
|
|||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import okio.use
|
||||
import javax.inject.Inject
|
||||
|
||||
class AndroidFileReader @Inject constructor(@ApplicationContext private val context: Context) : FileReader {
|
||||
|
|
@ -16,7 +17,10 @@ class AndroidFileReader @Inject constructor(@ApplicationContext private val cont
|
|||
|
||||
override fun rewriteFile(content: String, fileName: String) {
|
||||
context.openFileOutput(fileName, Context.MODE_PRIVATE).use { stream ->
|
||||
stream.write(content.toByteArray(), 0, content.length)
|
||||
stream.writer().use { writer ->
|
||||
// warning: don't write byteArray as json, its break cyrillic encoding
|
||||
writer.write(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ internal class BalanceStateHidingSettingsStore(
|
|||
return getSyncOrNull() ?: BalanceHidingSettings(
|
||||
isHidingEnabledInSettings = false,
|
||||
isBalanceHidden = false,
|
||||
isBalanceHidingNotificationEnabled = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,20 +5,26 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
|
|||
import com.tangem.domain.tokens.model.NetworkStatus
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
internal class DefaultNetworksStatusesStore(
|
||||
dataStore: StringKeyDataStore<Set<NetworkStatus>>,
|
||||
) : NetworksStatusesStore, StringKeyDataStoreDecorator<UserWalletId, Set<NetworkStatus>>(dataStore) {
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
override fun provideStringKey(key: UserWalletId): String {
|
||||
return key.stringValue
|
||||
}
|
||||
|
||||
override suspend fun store(key: UserWalletId, value: NetworkStatus) {
|
||||
val newValues = getSyncOrNull(key)
|
||||
?.addOrReplace(value) { it.network == value.network }
|
||||
?: setOf(value)
|
||||
mutex.withLock {
|
||||
val newValues = getSyncOrNull(key)
|
||||
?.addOrReplace(value) { it.network == value.network }
|
||||
?: setOf(value)
|
||||
|
||||
store(key, newValues)
|
||||
store(key, newValues)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.datasource.local.preferences
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.MutablePreferences
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import com.squareup.moshi.Moshi
|
||||
|
||||
/**
|
||||
* Application preferences store.
|
||||
* AppPreferencesStore is wrapper around DataStore<Preferences> that supports json serialization and deserialization.
|
||||
*
|
||||
* @property moshi Moshi instance. Property has 'public' modifier because it is used
|
||||
* by Public-API inline function. Don't use it directly.
|
||||
* @property preferencesDataStore DataStore<Preferences> instance
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AppPreferencesStore(
|
||||
val moshi: Moshi,
|
||||
private val preferencesDataStore: DataStore<Preferences>,
|
||||
) : DataStore<Preferences> by preferencesDataStore {
|
||||
|
||||
/**
|
||||
* Edit data according with transaction [transform].
|
||||
*
|
||||
* @param transform transaction. It has receiver [AppPreferencesStore] that allows to use [getObject], [setObject]
|
||||
* functions when creating transaction.
|
||||
*/
|
||||
suspend fun editData(transform: suspend AppPreferencesStore.(MutablePreferences) -> Unit): Preferences {
|
||||
return edit { transform(it) }
|
||||
}
|
||||
|
||||
/** Get nullable data [T] by string [key] from [MutablePreferences] */
|
||||
inline fun <reified T> MutablePreferences.getObject(key: Preferences.Key<String>): T? {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
return this[key]?.let(adapter::fromJson)
|
||||
}
|
||||
|
||||
/** Set data [T] by string [key] to [MutablePreferences] */
|
||||
inline fun <reified T> MutablePreferences.setObject(key: Preferences.Key<String>, value: T) {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
this[key] = adapter.toJson(value)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.datasource.local.preferences
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataMigration
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import com.tangem.datasource.local.preferences.PreferencesDataStore.INSTANCE
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Application preferences data store 'DataStore<Preferences>'.
|
||||
* Implements the singleton pattern [INSTANCE] under the hood.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object PreferencesDataStore {
|
||||
|
||||
private const val PREFERENCES_FILE_NAME = "TAP_PREFS"
|
||||
|
||||
private var INSTANCE: DataStore<Preferences>? = null
|
||||
|
||||
fun getInstance(context: Context, dispatcher: CoroutineContext): DataStore<Preferences> {
|
||||
return INSTANCE ?: create(context, dispatcher).also { INSTANCE = it }
|
||||
}
|
||||
|
||||
private fun create(context: Context, dispatcher: CoroutineContext): DataStore<Preferences> {
|
||||
return PreferenceDataStoreFactory.create(
|
||||
corruptionHandler = createCorruptionHandler(),
|
||||
migrations = createMigrations(),
|
||||
scope = CoroutineScope(context = dispatcher + SupervisorJob()),
|
||||
produceFile = { context.preferencesDataStoreFile(name = PREFERENCES_FILE_NAME) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun createCorruptionHandler(): ReplaceFileCorruptionHandler<Preferences> {
|
||||
return ReplaceFileCorruptionHandler(
|
||||
produceNewData = {
|
||||
Timber.w(it)
|
||||
emptyPreferences()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMigrations(): List<DataMigration<Preferences>> {
|
||||
return listOf()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.local.preferences
|
||||
|
||||
/**
|
||||
* All preferences keys that DataStore<Preferences> is stored.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object PreferencesKeys
|
||||
|
||||
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */
|
||||
internal fun getTapPrefKeysToMigrate(): Set<String> {
|
||||
return setOf()
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.datasource.local.preferences.utils
|
||||
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/** Get flow of nullable data [T] by string [key] */
|
||||
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>): Flow<T?> {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
return data.map { it[key]?.let(adapter::fromJson) }
|
||||
}
|
||||
|
||||
/** Get flow of data [T] by string [key]. If data is not found, it returns [default] */
|
||||
inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String>, default: T): Flow<T> {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
return data.map { it[key]?.let(adapter::fromJson) ?: default }
|
||||
}
|
||||
|
||||
/** Get nullable data [T] by string [key] */
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key<String>): T? {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
return data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
}
|
||||
|
||||
/** Get data [T] by string [key]. If data is not found, it returns [default] */
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectSyncOrDefault(
|
||||
key: Preferences.Key<String>,
|
||||
default: T,
|
||||
): T {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
return data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
?: default
|
||||
}
|
||||
|
||||
/** Store data [value] by string [key] */
|
||||
suspend inline fun <reified T> AppPreferencesStore.storeObject(key: Preferences.Key<String>, value: T) {
|
||||
val adapter = moshi.adapter(T::class.java)
|
||||
edit { it[key] = adapter.toJson(value) }
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.datasource.local.preferences.utils
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/** Get flow of nullable data [T] by [key] */
|
||||
fun <T> DataStore<Preferences>.get(key: Preferences.Key<T>): Flow<T?> {
|
||||
return data.map { it[key] }
|
||||
}
|
||||
|
||||
/** Get flow of data [T] by [key]. If data is not found, it returns [default] */
|
||||
fun <T> DataStore<Preferences>.get(key: Preferences.Key<T>, default: T): Flow<T> {
|
||||
return data.map { it[key] ?: default }
|
||||
}
|
||||
|
||||
/** Get nullable data [T] by [key] */
|
||||
suspend fun <T> DataStore<Preferences>.getSyncOrNull(key: Preferences.Key<T>): T? {
|
||||
return data.firstOrNull()?.get(key)
|
||||
}
|
||||
|
||||
/** Get data [T] by [key]. If data is not found, it returns [default] */
|
||||
suspend fun <T> DataStore<Preferences>.getSyncOrDefault(key: Preferences.Key<T>, default: T): T {
|
||||
return data.firstOrNull()?.get(key) ?: default
|
||||
}
|
||||
|
||||
/** Store data [value] by [key] */
|
||||
suspend fun <T> DataStore<Preferences>.store(key: Preferences.Key<T>, value: T) {
|
||||
edit { it[key] = value }
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package com.tangem.datasource.local.preferences.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.DoNotInline
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.datastore.core.DataMigration
|
||||
import androidx.datastore.preferences.core.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Migration of a specified key with name changing.
|
||||
* Example, migrate the "key1" from "pref1" to the "key2" from "pref2".
|
||||
*
|
||||
* @property context context
|
||||
* @property legacyPrefsName legacy SharedPreferences name
|
||||
* @property legacyKeyName legacy SharedPreferences key name
|
||||
* @property keyName new SharedPreferences key name
|
||||
*/
|
||||
internal class SharedPreferencesKeyMigration(
|
||||
private val context: Context,
|
||||
private val legacyPrefsName: String,
|
||||
private val legacyKeyName: String,
|
||||
private val keyName: String,
|
||||
) : DataMigration<Preferences> {
|
||||
|
||||
private val legacyPrefs = context.getSharedPreferences(legacyPrefsName, Context.MODE_PRIVATE)
|
||||
|
||||
override suspend fun cleanUp() {
|
||||
val sharedPrefsEditor = legacyPrefs.edit()
|
||||
|
||||
sharedPrefsEditor.remove(legacyKeyName)
|
||||
|
||||
if (!sharedPrefsEditor.commit()) {
|
||||
throw IOException("Unable to delete migrated keys from SharedPreferences.")
|
||||
}
|
||||
|
||||
if (legacyPrefs.all.isEmpty()) {
|
||||
deleteSharedPreferences(context = context, name = legacyPrefsName)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun shouldMigrate(currentData: Preferences): Boolean = true
|
||||
|
||||
override suspend fun migrate(currentData: Preferences): Preferences {
|
||||
val currentKeys = currentData.asMap().keys.map(Preferences.Key<*>::name)
|
||||
|
||||
// If migration is already happened, return
|
||||
if (currentKeys.contains(keyName)) return currentData
|
||||
|
||||
val value = legacyPrefs.all[legacyKeyName]
|
||||
if (value != null) {
|
||||
val mutablePreferences = currentData.toMutablePreferences()
|
||||
|
||||
when (value) {
|
||||
is Boolean -> mutablePreferences[booleanPreferencesKey(keyName)] = value
|
||||
is Float -> mutablePreferences[floatPreferencesKey(keyName)] = value
|
||||
is Int -> mutablePreferences[intPreferencesKey(keyName)] = value
|
||||
is Long -> mutablePreferences[longPreferencesKey(keyName)] = value
|
||||
is String -> mutablePreferences[stringPreferencesKey(keyName)] = value
|
||||
is Set<*> -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
mutablePreferences[stringSetPreferencesKey(keyName)] = value as Set<String>
|
||||
}
|
||||
}
|
||||
|
||||
return mutablePreferences.toPreferences()
|
||||
}
|
||||
|
||||
return currentData
|
||||
}
|
||||
|
||||
private fun deleteSharedPreferences(context: Context, name: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
if (!Api24Impl.deleteSharedPreferences(context, name)) {
|
||||
throw IOException("Unable to delete SharedPreferences: $name")
|
||||
}
|
||||
} else {
|
||||
val prefsFile = getSharedPrefsFile(context, name)
|
||||
val prefsBackup = getSharedPrefsBackup(prefsFile)
|
||||
|
||||
prefsFile.delete()
|
||||
prefsBackup.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private object Api24Impl {
|
||||
|
||||
@JvmStatic
|
||||
@DoNotInline
|
||||
fun deleteSharedPreferences(context: Context, name: String): Boolean {
|
||||
return context.deleteSharedPreferences(name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSharedPrefsFile(context: Context, name: String): File {
|
||||
val prefsDir = File(context.applicationInfo.dataDir, "shared_prefs")
|
||||
return File(prefsDir, "$name.xml")
|
||||
}
|
||||
|
||||
private fun getSharedPrefsBackup(prefsFile: File) = File(prefsFile.path + ".bak")
|
||||
}
|
||||
|
|
@ -4,9 +4,8 @@ import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
|||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.datasource.local.quote.model.StoredQuote
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
internal class DefaultQuotesStore(
|
||||
private val dataStore: StringKeyDataStore<StoredQuote>,
|
||||
|
|
@ -22,7 +21,12 @@ internal class DefaultQuotesStore(
|
|||
send(emptySet())
|
||||
}
|
||||
|
||||
combine(flows) { quotes -> quotes.toSet() }.collect(::send)
|
||||
merge(*flows.toTypedArray())
|
||||
.scan<StoredQuote, Set<StoredQuote>>(emptySet()) { acc, quote ->
|
||||
acc.addOrReplace(quote) { it.rawCurrencyId == quote.rawCurrencyId }
|
||||
}
|
||||
.filter(Set<StoredQuote>::isNotEmpty)
|
||||
.collect(::send)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,6 @@ internal class DefaultUserTokensStore(
|
|||
) : UserTokensStore, StringKeyDataStoreDecorator<UserWalletId, UserTokensResponse>(dataStore) {
|
||||
|
||||
override fun provideStringKey(key: UserWalletId): String {
|
||||
return key.stringValue
|
||||
return "user_tokens_${key.stringValue}"
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
|||
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import com.tangem.utils.extensions.removeBy
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class DefaultWalletManagersStore(
|
||||
|
|
@ -43,10 +44,18 @@ internal class DefaultWalletManagersStore(
|
|||
val updatedWalletManagers = walletManagers
|
||||
?.addOrReplace(walletManager) {
|
||||
it.wallet.blockchain == walletManager.wallet.blockchain &&
|
||||
it.wallet.publicKey == walletManager.wallet.publicKey
|
||||
it.wallet.publicKey.derivationPath == walletManager.wallet.publicKey.derivationPath
|
||||
}
|
||||
?: listOf(walletManager)
|
||||
|
||||
store(userWalletId, updatedWalletManagers)
|
||||
}
|
||||
|
||||
override suspend fun remove(userWalletId: UserWalletId, predicate: (WalletManager) -> Boolean) {
|
||||
val walletManagers = getSyncOrNull(userWalletId)?.toMutableList() ?: return
|
||||
|
||||
walletManagers.removeBy(predicate)
|
||||
|
||||
store(userWalletId, walletManagers)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,5 +19,7 @@ interface WalletManagersStore {
|
|||
|
||||
suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager)
|
||||
|
||||
suspend fun remove(userWalletId: UserWalletId, predicate: (WalletManager) -> Boolean)
|
||||
|
||||
suspend fun clear()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue