Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-20 12:21:24 +03:00
commit 2da48c037a
961 changed files with 24212 additions and 13399 deletions

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.api.common
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
import org.joda.time.LocalDate
import org.joda.time.format.DateTimeFormat
class LocalDateAdapter : JsonAdapter<LocalDate>() {
private val formatter = DateTimeFormat.forPattern("yyyy-MM-dd")
@FromJson
override fun fromJson(reader: JsonReader): LocalDate? {
val dateString = reader.nextString()
return LocalDate.parse(dateString, formatter)
}
@ToJson
override fun toJson(writer: JsonWriter, value: LocalDate?) {
if (value != null) {
writer.value(formatter.print(value))
} else {
writer.nullValue()
}
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.datasource.api.common.response
/**
* Represents the possible responses from an API request.
*
* @param T The type of the data that is expected in a successful response.
*/
sealed class ApiResponse<T : Any> {
/**
* Represents a successful response from the API.
*
* @property data The data returned by the API.
*/
data class Success<T : Any>(val data: T) : ApiResponse<T>()
/**
* Represents an error response or failure from the API.
*
* @property cause The cause of the error.
*/
data class Error(val cause: ApiResponseError) : ApiResponse<Nothing>()
}
/**
* Wraps data in a [ApiResponse.Success] instance.
*
* @param data The data to wrap.
* @return A [ApiResponse.Success] instance containing the provided data.
*/
internal fun <T : Any> apiSuccess(data: T): ApiResponse<T> = ApiResponse.Success(data)
/**
* Wraps an [ApiResponseError] in a [ApiResponse.Error] instance.
*
* @param cause The error to wrap.
* @return A [ApiResponse.Error] instance containing the provided error.
*/
@Suppress("UNCHECKED_CAST")
internal fun <T : Any> apiError(cause: ApiResponseError): ApiResponse<T> = ApiResponse.Error(cause) as ApiResponse<T>

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.api.common.response
import retrofit2.Call
import retrofit2.CallAdapter
import java.lang.reflect.Type
internal class ApiResponseCallAdapter(
private val resultType: Type,
) : CallAdapter<Type, Call<ApiResponse<Type>>> {
override fun responseType(): Type = resultType
override fun adapt(call: Call<Type>): Call<ApiResponse<Type>> {
return ApiResponseCallDelegate(call)
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.api.common.response
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
internal class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
if (getRawType(returnType) != Call::class.java) {
return null
}
val callType = getParameterUpperBound(0, returnType as ParameterizedType)
if (getRawType(callType) != ApiResponse::class.java) {
return null
}
val resultType = getParameterUpperBound(0, callType as ParameterizedType)
return ApiResponseCallAdapter(resultType)
}
companion object {
fun create() = ApiResponseCallAdapterFactory()
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.datasource.api.common.response
import okhttp3.Request
import okio.Timeout
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
internal class ApiResponseCallDelegate<T : Any>(
private val wrappedCall: Call<T>,
) : Call<ApiResponse<T>> {
override fun enqueue(callback: Callback<ApiResponse<T>>) {
wrappedCall.enqueue(ApiResponseCallback(callback))
}
override fun execute(): Response<ApiResponse<T>> = throw NotImplementedError()
override fun clone(): Call<ApiResponse<T>> = ApiResponseCallDelegate(wrappedCall.clone())
override fun request(): Request = wrappedCall.request()
override fun timeout(): Timeout = wrappedCall.timeout()
override fun isExecuted(): Boolean = wrappedCall.isExecuted
override fun isCanceled(): Boolean = wrappedCall.isCanceled
override fun cancel() { wrappedCall.cancel() }
private inner class ApiResponseCallback(
private val responseCallback: Callback<ApiResponse<T>>,
) : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val safeResponse = response.toSafeApiResponse()
responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse))
}
override fun onFailure(call: Call<T>, t: Throwable) {
val e = if (t.isNetworkException()) {
ApiResponseError.NetworkException
} else {
ApiResponseError.UnknownException(t)
}
val safeResponse = apiError<T>(e)
responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse))
}
}
}

View file

@ -0,0 +1,80 @@
package com.tangem.datasource.api.common.response
/**
* Represents the possible errors that can occur during an API request.
*/
sealed class ApiResponseError : Exception() {
/**
* Represents an HTTP exception, which typically occurs when the server responds
* with a non-2xx HTTP status code.
*
* @property code The HTTP status code.
* @property message A human-readable message describing the error.
*/
data class HttpException(val code: Code, override val message: String?) : ApiResponseError() {
// region Error Codes
enum class Code(val code: Int) {
// 4xx Server Errors
BAD_REQUEST(code = 400),
UNAUTHORIZED(code = 401),
PAYMENT_REQUIRED(code = 402),
FORBIDDEN(code = 403),
NOT_FOUND(code = 404),
METHOD_NOT_ALLOWED(code = 405),
NOT_ACCEPTABLE(code = 406),
PROXY_AUTHENTICATION_REQUIRED(code = 407),
REQUEST_TIMEOUT(code = 408),
CONFLICT(code = 409),
GONE(code = 410),
LENGTH_REQUIRED(code = 411),
PRECONDITION_FAILED(code = 412),
PAYLOAD_TOO_LARGE(code = 413),
URI_TOO_LONG(code = 414),
UNSUPPORTED_MEDIA_TYPE(code = 415),
RANGE_NOT_SATISFIABLE(code = 416),
EXPECTATION_FAILED(code = 417),
IM_A_TEAPOT(code = 418), // Not an error, but an April Fools' joke from RFC 2324
UNPROCESSABLE_ENTITY(code = 422),
LOCKED(code = 423),
FAILED_DEPENDENCY(code = 424),
TOO_EARLY(code = 425),
UPGRADE_REQUIRED(code = 426),
PRECONDITION_REQUIRED(code = 428),
TOO_MANY_REQUESTS(code = 429),
REQUEST_HEADER_FIELDS_TOO_LARGE(code = 431),
UNAVAILABLE_FOR_LEGAL_REASONS(code = 451),
// 5xx Server Errors
INTERNAL_SERVER_ERROR(code = 500),
NOT_IMPLEMENTED(code = 501),
BAD_GATEWAY(code = 502),
SERVICE_UNAVAILABLE(code = 503),
GATEWAY_TIMEOUT(code = 504),
HTTP_VERSION_NOT_SUPPORTED(code = 505),
VARIANT_ALSO_NEGOTIATES(code = 506),
INSUFFICIENT_STORAGE(code = 507),
LOOP_DETECTED(code = 508),
NOT_EXTENDED(code = 510),
NETWORK_AUTHENTICATION_REQUIRED(code = 511),
;
override fun toString(): String = "$code - $name"
companion object {
val values = values()
}
}
// endregion Error Codes
}
/** Represents a network error, typically when there's no connectivity. */
object NetworkException : ApiResponseError()
/**
* Represents an unexpected exception that doesn't fall into one of the other categories.
*
* @property cause The exception that caused this error.
*/
data class UnknownException(override val cause: Throwable) : ApiResponseError()
}

View file

@ -0,0 +1,6 @@
package com.tangem.datasource.api.common.response
fun <T : Any> ApiResponse<T>.getOrThrow(): T = when (this) {
is ApiResponse.Error -> throw cause
is ApiResponse.Success -> data
}

View file

@ -0,0 +1,32 @@
package com.tangem.datasource.api.common.response
import retrofit2.Response
import java.net.ConnectException
import java.net.UnknownHostException
import javax.net.ssl.SSLHandshakeException
internal fun <T : Any> Response<T>.toSafeApiResponse(): ApiResponse<T> {
val body = body()
return if (isSuccessful && body != null) {
apiSuccess(body)
} else {
val code = ApiResponseError.HttpException.Code.values
.firstOrNull { it.code == code() }
val e = if (code == null) {
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))
} else {
ApiResponseError.HttpException(code, message())
}
apiError(e)
}
}
internal fun Throwable.isNetworkException(): Boolean = when (this) {
is ConnectException,
is UnknownHostException,
is SSLHandshakeException,
-> true
else -> false
}

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.tangemTech.models.*
import retrofit2.http.*
@ -25,13 +26,13 @@ interface TangemTechApi {
suspend fun getRates(@Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String): RatesResponse
@GET("currencies")
suspend fun getCurrencyList(): CurrenciesResponse
suspend fun getCurrencyList(): ApiResponse<CurrenciesResponse>
@GET("geo")
suspend fun getUserCountryCode(): GeoResponse
@GET("user-tokens/{user-id}")
suspend fun getUserTokens(@Path(value = "user-id") userId: String): UserTokensResponse
suspend fun getUserTokens(@Path(value = "user-id") userId: String): ApiResponse<UserTokensResponse>
@PUT("user-tokens/{user-id}")
suspend fun saveUserTokens(@Path(value = "user-id") userId: String, @Body userTokens: UserTokensResponse)
@ -66,5 +67,5 @@ interface TangemTechApi {
@Query("currencyId") currencyId: String,
@Query("coinIds") coinIds: String,
@Query("fields") fields: String = "price,priceChange24h,lastUpdatedAt",
): QuotesResponse
): ApiResponse<QuotesResponse>
}

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.datasource.utils.RequestHeader
import com.tangem.datasource.utils.RequestHeader.AuthenticationHeader
import com.tangem.datasource.utils.RequestHeader.CacheControlHeader
@ -29,6 +30,7 @@ object TangemTechService {
val headers = mutableListOf<RequestHeader>(CacheControlHeader).apply { header?.let(::add) }
return Retrofit.Builder()
.addConverterFactory(MoshiConverter.networkMoshiConverter)
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.baseUrl(TANGEM_TECH_BASE_URL)
.client(
OkHttpClient.Builder()

View file

@ -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?,
)
}

View file

@ -1,14 +1,12 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import org.joda.time.LocalDate
/**
* Main response class for referral API
* contains all necessary info about users program status
*/
data class ReferralResponse(
@Json(name = "conditions") val conditions: Conditions,
@Json(name = "referral") val referral: Referral?,
@Json(name = "expectedAwards") val expectedAwards: ExpectedAwards?,
) {
data class Conditions(
@ -45,4 +43,16 @@ data class ReferralResponse(
@Json(name = "walletsPurchased") val walletsPurchased: Int,
@Json(name = "termsAcceptedAt") val termsAcceptedAt: String?,
)
data class ExpectedAwards(
@Json(name = "numberOfWallets") val numberOfWallets: Int,
@Json(name = "list") val list: List<AwardItem>,
) {
data class AwardItem(
@Json(name = "currency") val currency: String,
@Json(name = "paymentDate") val paymentDate: LocalDate,
@Json(name = "amount") val amount: Int,
)
}
}

View file

@ -7,8 +7,8 @@ import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.datasource.local.appcurrency.implementation.DefaultAvailableAppCurrenciesStore
import com.tangem.datasource.local.appcurrency.implementation.DefaultSelectedAppCurrencyStore
import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.datastore.SharedPreferencesDataStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -35,7 +35,7 @@ internal object AppCurrencyDataModule {
@NetworkMoshi moshi: Moshi,
): SelectedAppCurrencyStore {
return DefaultSelectedAppCurrencyStore(
dataStore = SharedPreferencesDataStore(
dataStore = JsonSharedPreferencesDataStore(
preferencesName = "selected_app_currency",
context = context,
adapter = moshi.adapter(CurrenciesResponse.Currency::class.java),

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.apptheme.AppThemeModeStore
import com.tangem.datasource.local.apptheme.DefaultAppThemeModeStore
import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore
import com.tangem.domain.apptheme.model.AppThemeMode
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal object AppThemeModeDataModule {
@Provides
fun provideAppThemeModeStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): AppThemeModeStore {
return DefaultAppThemeModeStore(
dataStore = JsonSharedPreferencesDataStore(
preferencesName = "app_theme",
context = context,
adapter = moshi.adapter(AppThemeMode::class.java),
),
)
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.datasource.local.card.DefaultUsedCardsStore
import com.tangem.datasource.local.card.UsedCardInfo
import com.tangem.datasource.local.card.UsedCardsStore
import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore
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 CardDataModule {
@Provides
@Singleton
fun provideUsedCardsStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): UsedCardsStore {
return DefaultUsedCardsStore(
store = JsonSharedPreferencesDataStore(
preferencesName = "tapPrefs",
context = context,
adapter = moshi.adapter(
Types.newParameterizedType(List::class.java, UsedCardInfo::class.java),
),
),
)
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore
import com.tangem.datasource.local.appcurrency.implementation.BalanceStateHidingSettingsStore
import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore
import com.tangem.domain.balancehiding.BalanceHidingSettings
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal object HiddenBalanceDataModule {
@Provides
fun provideHiddenBalanceStateStore(
@ApplicationContext context: Context,
@NetworkMoshi moshi: Moshi,
): BalanceHidingSettingsStore {
return BalanceStateHidingSettingsStore(
dataStore = JsonSharedPreferencesDataStore(
preferencesName = "balance_hiding_settings",
context = context,
adapter = moshi.adapter(BalanceHidingSettings::class.java),
),
)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.token.DefaultUserMarketCoinsStore
import com.tangem.datasource.local.token.UserMarketCoinsStore
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 MarketCoinsStoreModule {
@Provides
@Singleton
fun provideUserMarketCoinsStore(): UserMarketCoinsStore {
return DefaultUserMarketCoinsStore(dataStore = RuntimeDataStore())
}
}

View file

@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.BigDecimalAdapter
import com.tangem.datasource.api.common.LocalDateAdapter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -20,6 +21,7 @@ class MoshiModule {
fun provideNetworkMoshi(): Moshi {
return Moshi.Builder()
.add(BigDecimalAdapter())
.add(LocalDateAdapter())
.add(KotlinJsonAdapterFactory())
.build()
}

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.di
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.datasource.api.paymentology.PaymentologyApi
import com.tangem.datasource.api.promotion.PromotionApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -8,7 +9,6 @@ import com.tangem.datasource.utils.RequestHeader.*
import com.tangem.datasource.utils.addHeaders
import com.tangem.datasource.utils.allowLogging
import com.tangem.lib.auth.AuthProvider
import com.tangem.lib.auth.BuildConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -28,6 +28,7 @@ class NetworkModule {
fun provideTangemTechApi(@NetworkMoshi moshi: Moshi): TangemTechApi {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.baseUrl(PROD_TANGEM_TECH_BASE_URL)
.client(
OkHttpClient.Builder()
@ -76,7 +77,7 @@ class NetworkModule {
private fun createBasePromotionRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): PromotionApi {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(if (BuildConfig.DEBUG) DEV_TANGEM_TECH_BASE_URL else PROD_TANGEM_TECH_BASE_URL)
.baseUrl(PROD_TANGEM_TECH_BASE_URL)
.client(okHttpClient)
.build()
.create(PromotionApi::class.java)

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.network.DefaultNetworksStatusesStore
import com.tangem.datasource.local.network.NetworksStatusesStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal object NetworksStatusesStoreModule {
@Provides
fun provideNetworksStatusesStore(): NetworksStatusesStore {
return DefaultNetworksStatusesStore(
dataStore = RuntimeDataStore(),
)
}
}

View file

@ -1,15 +1,11 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.datastore.SharedPreferencesDataStore
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.quote.DefaultQuotesStore
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.local.quote.model.StoredQuote
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
@ -19,13 +15,9 @@ internal object QuotesStoreModule {
@Provides
@Singleton
fun provideQuotesStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): QuotesStore {
fun provideQuotesStore(): QuotesStore {
return DefaultQuotesStore(
dataStore = SharedPreferencesDataStore(
preferencesName = "quotes",
context = context,
adapter = moshi.adapter(StoredQuote::class.java),
),
dataStore = RuntimeDataStore(),
)
}
}

View file

@ -0,0 +1,50 @@
package com.tangem.datasource.di
import android.content.Context
import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.IntSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.LongSharedPreferencesDataStore
import com.tangem.datasource.local.settings.*
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 SettingsDataModule {
@Provides
@Singleton
fun provideAppLaunchCountStore(@ApplicationContext context: Context): AppLaunchCountStore {
return DefaultAppLaunchCountStore(
store = IntSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context),
)
}
@Provides
@Singleton
fun provideAppRatingShowingCountStore(@ApplicationContext context: Context): AppRatingShowingCountStore {
return DefaultAppRatingShowingCountStore(
store = IntSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context),
)
}
@Provides
@Singleton
fun provideFundsFoundDateInMillisStore(@ApplicationContext context: Context): FundsFoundDateInMillisStore {
return DefaultFundsFoundDateInMillisStore(
store = LongSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context),
)
}
@Provides
@Singleton
fun provideUserInteractingStatusStore(@ApplicationContext context: Context): UserInteractingStatusStore {
return DefaultUserInteractingStatusStore(
store = BooleanSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context),
)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.txhistory.DefaultTxHistoryItemsStore
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal object TxHistoryItemsStoreModule {
@Provides
fun provideTxHistoryItemsStore(): TxHistoryItemsStore {
return DefaultTxHistoryItemsStore(
dataStore = RuntimeDataStore(),
)
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.datasource.di
import android.content.Context
import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore
import com.tangem.datasource.local.settings.*
import com.tangem.datasource.local.userwallet.DefaultShouldSaveUserWalletStore
import com.tangem.datasource.local.userwallet.ShouldSaveUserWalletStore
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 WalletsDataModule {
@Provides
@Singleton
fun provideShouldSaveUserWalletsStore(@ApplicationContext context: Context): ShouldSaveUserWalletStore {
return DefaultShouldSaveUserWalletStore(
store = BooleanSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context),
)
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.local.appcurrency
import com.tangem.domain.balancehiding.BalanceHidingSettings
import kotlinx.coroutines.flow.Flow
interface BalanceHidingSettingsStore {
fun get(): Flow<BalanceHidingSettings>
suspend fun getSyncOrDefault(): BalanceHidingSettings
suspend fun store(settings: BalanceHidingSettings)
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.local.appcurrency.implementation
import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.balancehiding.BalanceHidingSettings
internal class BalanceStateHidingSettingsStore(
dataStore: StringKeyDataStore<BalanceHidingSettings>,
) : BalanceHidingSettingsStore, KeylessDataStoreDecorator<BalanceHidingSettings>(dataStore) {
override suspend fun getSyncOrDefault(): BalanceHidingSettings {
return getSyncOrNull() ?: BalanceHidingSettings(
isHidingEnabledInSettings = false,
isBalanceHidden = false,
)
}
}

View file

@ -7,8 +7,4 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore
internal class DefaultSelectedAppCurrencyStore(
dataStore: StringKeyDataStore<CurrenciesResponse.Currency>,
) : SelectedAppCurrencyStore, KeylessDataStoreDecorator<CurrenciesResponse.Currency>(dataStore) {
override suspend fun isEmpty(): Boolean {
return getSyncOrNull() == null
}
}
) : SelectedAppCurrencyStore, KeylessDataStoreDecorator<CurrenciesResponse.Currency>(dataStore)

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.local.apptheme
import com.tangem.domain.apptheme.model.AppThemeMode
import kotlinx.coroutines.flow.Flow
interface AppThemeModeStore {
fun get(): Flow<AppThemeMode>
suspend fun store(item: AppThemeMode)
suspend fun isEmpty(): Boolean
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.local.apptheme
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.apptheme.model.AppThemeMode
internal class DefaultAppThemeModeStore(
dataStore: StringKeyDataStore<AppThemeMode>,
) : AppThemeModeStore, KeylessDataStoreDecorator<AppThemeMode>(dataStore)

View file

@ -10,5 +10,7 @@ interface CacheKeysStore {
suspend fun remove(key: String)
suspend fun remove(keys: Collection<String>)
suspend fun clear()
}

View file

@ -2,15 +2,10 @@ package com.tangem.datasource.local.cache
import com.tangem.datasource.local.cache.model.CacheKey
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
internal class DefaultCacheKeysStore(
dataStore: StringKeyDataStore<CacheKey>,
) : CacheKeysStore, StringKeyDataStoreDecorator<String, CacheKey>(dataStore) {
override fun provideStringKey(key: String): String {
return key
}
) : CacheKeysStore, StringKeyDataStore<CacheKey> by dataStore {
override suspend fun store(key: CacheKey) {
store(key.id, key)

View file

@ -0,0 +1,8 @@
package com.tangem.datasource.local.card
data class UsedCardInfo(
val cardId: String,
val isScanned: Boolean = false,
val isActivationStarted: Boolean = false,
val isActivationFinished: Boolean = false,
)

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.local.card
import com.tangem.datasource.local.datastore.SharedPreferencesDataStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import kotlinx.coroutines.flow.Flow
interface UsedCardsStore {
fun get(): Flow<List<UsedCardInfo>>
suspend fun getSyncOrNull(): List<UsedCardInfo>?
suspend fun store(item: List<UsedCardInfo>)
}
internal class DefaultUsedCardsStore(
store: SharedPreferencesDataStore<List<UsedCardInfo>>,
) : UsedCardsStore, KeylessDataStoreDecorator<List<UsedCardInfo>>(
wrappedDataStore = store,
key = "usedCardsInfo_v2",
)

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.local.datastore
import android.content.Context
import androidx.core.content.edit
internal class BooleanSharedPreferencesDataStore(
preferencesName: String,
context: Context,
) : SharedPreferencesDataStore<Boolean>(preferencesName, context) {
override fun getByKey(key: String): Boolean = sharedPreferences.getBoolean(key, false)
override fun storeByKey(key: String, value: Boolean) {
sharedPreferences.edit { putBoolean(key, value) }
}
}

View file

@ -4,7 +4,10 @@ import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.files.FileReader
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.utils.Trigger
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import timber.log.Timber
@Deprecated("Use shared preferences data store instead")
@ -14,6 +17,14 @@ internal class FileDataStore<Value : Any>(
) : StringKeyDataStore<Value> {
private val writeTrigger = Trigger()
override suspend fun isEmpty(): Boolean {
val e = NotImplementedError("`isEmpty()` function not implemented for `FileDataStore`")
Timber.e(e)
throw e
}
override suspend fun contains(key: String): Boolean = getSyncOrNull(key) != null
override fun get(key: String): Flow<Value> {
return writeTrigger
@ -33,16 +44,16 @@ internal class FileDataStore<Value : Any>(
return getInternal(key)
}
override suspend fun getAllSyncOrNull(): List<Value> {
override suspend fun getAllSyncOrNull(): List<Value>? {
val e = NotImplementedError("`getAllSyncOrNull()` function not implemented for `FileDataStore`")
Timber.e(e)
throw e
}
override suspend fun store(key: String, item: Value) {
override suspend fun store(key: String, value: Value) {
try {
val json = adapter.toJson(item)
val json = adapter.toJson(value)
fileReader.rewriteFile(json, key)
writeTrigger.trigger()
@ -51,8 +62,8 @@ internal class FileDataStore<Value : Any>(
}
}
override suspend fun store(items: Map<String, Value>) {
items.forEach { (key, item) ->
override suspend fun store(values: Map<String, Value>) {
values.forEach { (key, item) ->
store(key, item)
}
}
@ -62,6 +73,13 @@ internal class FileDataStore<Value : Any>(
writeTrigger.trigger()
}
override suspend fun remove(keys: Collection<String>) {
val e = NotImplementedError("`remove(keys)` function not implemented for `FileDataStore`")
Timber.e(e)
throw e
}
override suspend fun clear() {
val e = NotImplementedError("`clear()` function not implemented for `FileDataStore`")
Timber.e(e)

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.local.datastore
import android.content.Context
import androidx.core.content.edit
internal class IntSharedPreferencesDataStore(
preferencesName: String,
context: Context,
) : SharedPreferencesDataStore<Int>(preferencesName, context) {
override fun getByKey(key: String): Int = sharedPreferences.getInt(key, 0)
override fun storeByKey(key: String, value: Int) {
sharedPreferences.edit { putInt(key, value) }
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.datasource.local.datastore
import android.content.Context
import androidx.core.content.edit
import com.squareup.moshi.JsonAdapter
internal class JsonSharedPreferencesDataStore<Value : Any>(
preferencesName: String,
context: Context,
private val adapter: JsonAdapter<Value>,
) : SharedPreferencesDataStore<Value>(preferencesName, context) {
override fun getByKey(key: String): Value? {
val json = sharedPreferences.getString(key, null) ?: return null
return adapter.fromJson(json)
}
override fun storeByKey(key: String, value: Value) {
val json = adapter.toJson(value)
sharedPreferences.edit { putString(key, json) }
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.local.datastore
import android.content.Context
import androidx.core.content.edit
internal class LongSharedPreferencesDataStore(
preferencesName: String,
context: Context,
) : SharedPreferencesDataStore<Long>(preferencesName, context) {
override fun getByKey(key: String): Long = sharedPreferences.getLong(key, 0)
override fun storeByKey(key: String, value: Long) {
sharedPreferences.edit { putLong(key, value) }
}
}

View file

@ -5,53 +5,74 @@ import kotlinx.coroutines.flow.*
internal class RuntimeDataStore<Data : Any> : StringKeyDataStore<Data> {
private val store = MutableStateFlow<HashMap<String, Data>>(hashMapOf())
private val store: MutableSharedFlow<HashMap<String, Data>?> = MutableSharedFlow(replay = 1)
init {
store.tryEmit(value = null)
}
override suspend fun isEmpty(): Boolean = store.firstOrNull().isNullOrEmpty()
override suspend fun contains(key: String): Boolean = getSyncOrNull(key) != null
override fun get(key: String): Flow<Data> {
return store
.map { value -> value[key] }
.filterNotNull()
}
override fun getAll(): Flow<List<Data>> {
return store.map { value -> value.values.toList() }
}
override suspend fun getSyncOrNull(key: String): Data? {
return store.value[key]
}
override suspend fun getAllSyncOrNull(): List<Data> {
return store.value.values.toList()
}
override suspend fun store(key: String, item: Data) {
store.update { value ->
value[key] = item
value
return store.mapNotNull { value ->
value?.get(key)
}
}
override suspend fun store(items: Map<String, Data>) {
store.update { value ->
items.forEach { (key, item) ->
value[key] = item
}
override fun getAll(): Flow<List<Data>> {
return store.map { value ->
value?.values?.toList().orEmpty()
}
}
value
override suspend fun getSyncOrNull(key: String): Data? {
return store.firstOrNull()?.get(key)
}
override suspend fun getAllSyncOrNull(): List<Data>? {
return store.firstOrNull()?.values?.toList()
}
override suspend fun store(key: String, value: Data) {
updateValue { storedValue ->
storedValue[key] = value
storedValue
}
}
override suspend fun store(values: Map<String, Data>) {
updateValue { storedValue ->
storedValue.putAll(values)
storedValue
}
}
override suspend fun remove(key: String) {
store.update { value ->
value.remove(key)
updateValue { storedValue ->
storedValue.remove(key)
value
storedValue
}
}
override suspend fun remove(keys: Collection<String>) {
updateValue { value ->
HashMap(value.filterKeys { it !in keys })
}
}
override suspend fun clear() {
store.update { hashMapOf() }
store.emit(value = null)
}
private suspend inline fun updateValue(update: (HashMap<String, Data>) -> HashMap<String, Data>) {
val storedData = store.firstOrNull() ?: hashMapOf()
val updatedData = update(storedData)
store.emit(updatedData)
}
}

View file

@ -4,61 +4,63 @@ import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import androidx.core.content.edit
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.utils.Trigger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import timber.log.Timber
internal class SharedPreferencesDataStore<Value : Any>(
internal abstract class SharedPreferencesDataStore<Value : Any>(
preferencesName: String,
private val context: Context,
private val adapter: JsonAdapter<Value>,
context: Context,
) : StringKeyDataStore<Value> {
private val sharedPreferences: SharedPreferences by lazy {
protected val sharedPreferences: SharedPreferences by lazy {
context.getSharedPreferences(preferencesName, MODE_PRIVATE)
}
private val writeTrigger = Trigger()
abstract fun getByKey(key: String): Value?
abstract fun storeByKey(key: String, value: Value)
override suspend fun isEmpty(): Boolean {
return sharedPreferences.all.isEmpty()
}
override suspend fun contains(key: String): Boolean {
return sharedPreferences.contains(key)
}
override fun get(key: String): Flow<Value> {
return writeTrigger
.map { getInternal(key) }
.filterNotNull()
.mapNotNull { getInternal(key) }
.distinctUntilChanged()
}
override fun getAll(): Flow<List<Value>> {
return writeTrigger
.map { getAllInternal() }
.distinctUntilChanged()
throw UnsupportedOperationException("Unknown key")
}
override suspend fun getSyncOrNull(key: String): Value? {
return getInternal(key)
}
override suspend fun getSyncOrNull(key: String): Value? = getInternal(key)
override suspend fun getAllSyncOrNull(): List<Value> {
return getAllInternal()
throw UnsupportedOperationException("Unknown key")
}
override suspend fun store(key: String, item: Value) {
override suspend fun store(key: String, value: Value) {
try {
val json = adapter.toJson(item)
sharedPreferences.edit { putString(key, json) }
storeByKey(key, value)
writeTrigger.trigger()
} catch (e: Throwable) {
Timber.e(e, "Unable to edit preferences: $key")
}
}
override suspend fun store(items: Map<String, Value>) {
items.forEach { (key, item) ->
override suspend fun store(values: Map<String, Value>) {
values.forEach { (key, item) ->
store(key, item)
}
}
@ -68,32 +70,29 @@ internal class SharedPreferencesDataStore<Value : Any>(
writeTrigger.trigger()
}
override suspend fun remove(keys: Collection<String>) {
sharedPreferences.edit {
keys.forEach { key ->
remove(key)
}
}
writeTrigger.trigger()
}
override suspend fun clear() {
sharedPreferences.edit { clear() }
writeTrigger.trigger()
}
private fun getInternal(key: String): Value? {
return try {
val json = sharedPreferences.getString(key, null) ?: return null
if (!sharedPreferences.contains(key)) return null
adapter.fromJson(json)
return try {
getByKey(key)
} catch (e: Throwable) {
Timber.e(e, "Unable to get value from preferences: $key")
null
}
}
private fun getAllInternal(): List<Value> {
return sharedPreferences.all.mapNotNull { (key, value) ->
try {
val json = value as? String ?: return@mapNotNull null
adapter.fromJson(json)
} catch (e: Throwable) {
Timber.e(e, "Unable to convert value from JSON: $key")
null
}
}
}
}

View file

@ -2,21 +2,94 @@ package com.tangem.datasource.local.datastore.core
import kotlinx.coroutines.flow.Flow
/**
* Represents a generic key-value data store.
*
* @param Key The type of the keys used to identify values in the store.
* @param Value The type of the values stored.
*/
internal interface DataStore<Key : Any, Value : Any> {
/**
* Checks if the data store is empty.
*
* @return `true` if the data store has no entries, otherwise `false`.
*/
suspend fun isEmpty(): Boolean
/**
* Checks if the data store contains an entry with the specified key.
*
* @param key The key to check for presence in the store.
* @return `true` if the key is present, otherwise `false`.
*/
suspend fun contains(key: Key): Boolean
/**
* Retrieves a value updates associated with the given key, as a flow.
*
* @param key The key to look up in the store.
* @return A flow emitting the value associated with the given key.
*/
fun get(key: Key): Flow<Value>
/**
* Retrieves all values updates from the data store, as a flow.
*
* @return A flow emitting a list of all values in the store.
*/
fun getAll(): Flow<List<Value>>
/**
* Retrieves a value associated with the given key synchronously.
*
* If the key does not exist, this method returns `null`.
*
* @param key The key to look up in the store.
* @return The value associated with the key, or `null` if not present.
*/
suspend fun getSyncOrNull(key: Key): Value?
suspend fun getAllSyncOrNull(): List<Value>
/**
* Retrieves all values from the data store synchronously.
*
* If the store is empty, this method returns `null`.
*
* @return A list of all values in the store, or `null` if empty.
*/
suspend fun getAllSyncOrNull(): List<Value>?
suspend fun store(key: Key, item: Value)
/**
* Stores a value in the data store associated with the given key.
*
* @param key The key to associate with the value.
* @param value The value to store.
*/
suspend fun store(key: Key, value: Value)
suspend fun store(items: Map<Key, Value>)
/**
* Stores multiple values in the data store with their associated keys.
*
* @param values A map of keys to values to store.
*/
suspend fun store(values: Map<Key, Value>)
/**
* Removes a value associated with the given key from the data store.
*
* @param key The key of the value to remove.
*/
suspend fun remove(key: Key)
/**
* Removes values associated with the given keys from the data store.
*
* @param keys Keys of values to remove.
*/
suspend fun remove(keys: Collection<Key>)
/**
* Clears all entries from the data store.
*/
suspend fun clear()
}

View file

@ -1,28 +1,21 @@
package com.tangem.datasource.local.datastore.core
import kotlinx.coroutines.flow.Flow
internal abstract class KeylessDataStoreDecorator<Value : Any>(
wrappedDataStore: StringKeyDataStore<Value>,
) : StringKeyDataStoreDecorator<Unit, Value>(wrappedDataStore) {
private val key: String = DEFAULT_STRING_KEY,
) : StringKeyDataStoreDecorator<String, Value>(wrappedDataStore) {
override fun provideStringKey(key: Unit): String {
return STRING_KEY
}
override fun provideStringKey(key: String) = key
open fun get(): Flow<Value> {
return get(Unit)
}
open fun get() = get(key)
open suspend fun getSyncOrNull(): Value? {
return getSyncOrNull(Unit)
}
open suspend fun getSyncOrNull() = getSyncOrNull(key)
open suspend fun store(item: Value) {
store(Unit, item)
}
open suspend fun store(item: Value) = store(key, item)
override suspend fun isEmpty(): Boolean = getSyncOrNull() == null
private companion object {
const val STRING_KEY = "key"
const val DEFAULT_STRING_KEY = "key"
}
}

View file

@ -8,6 +8,10 @@ internal abstract class StringKeyDataStoreDecorator<Key : Any, Value : Any>(
abstract fun provideStringKey(key: Key): String
override suspend fun isEmpty(): Boolean = wrappedDataStore.isEmpty()
override suspend fun contains(key: Key): Boolean = wrappedDataStore.contains(provideStringKey(key))
override fun get(key: Key): Flow<Value> {
return wrappedDataStore.get(provideStringKey(key))
}
@ -16,7 +20,7 @@ internal abstract class StringKeyDataStoreDecorator<Key : Any, Value : Any>(
return wrappedDataStore.getAll()
}
override suspend fun getAllSyncOrNull(): List<Value> {
override suspend fun getAllSyncOrNull(): List<Value>? {
return wrappedDataStore.getAllSyncOrNull()
}
@ -24,13 +28,13 @@ internal abstract class StringKeyDataStoreDecorator<Key : Any, Value : Any>(
return wrappedDataStore.getSyncOrNull(provideStringKey(key))
}
override suspend fun store(key: Key, item: Value) {
wrappedDataStore.store(provideStringKey(key), item)
override suspend fun store(key: Key, value: Value) {
wrappedDataStore.store(provideStringKey(key), value)
}
override suspend fun store(items: Map<Key, Value>) {
override suspend fun store(values: Map<Key, Value>) {
wrappedDataStore.store(
items = items.mapKeys { (key, _) -> provideStringKey(key) },
values = values.mapKeys { (key, _) -> provideStringKey(key) },
)
}
@ -38,6 +42,10 @@ internal abstract class StringKeyDataStoreDecorator<Key : Any, Value : Any>(
wrappedDataStore.remove(provideStringKey(key))
}
override suspend fun remove(keys: Collection<Key>) {
wrappedDataStore.remove(keys.map(::provideStringKey))
}
override suspend fun clear() {
wrappedDataStore.clear()
}

View file

@ -0,0 +1,30 @@
package com.tangem.datasource.local.network
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
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) {
mutex.withLock {
val newValues = getSyncOrNull(key)
?.addOrReplace(value) { it.network == value.network }
?: setOf(value)
store(key, newValues)
}
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.local.network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
interface NetworksStatusesStore {
fun get(key: UserWalletId): Flow<Set<NetworkStatus>>
suspend fun getSyncOrNull(key: UserWalletId): Set<NetworkStatus>?
suspend fun store(key: UserWalletId, value: NetworkStatus)
}

View file

@ -3,8 +3,9 @@ package com.tangem.datasource.local.quote
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.models.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.combine
internal class DefaultQuotesStore(
@ -12,16 +13,24 @@ internal class DefaultQuotesStore(
) : QuotesStore {
override fun get(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<StoredQuote>> {
val flows = currenciesIds.mapNotNull { currencyId ->
dataStore.get(currencyId.rawCurrencyId ?: return@mapNotNull null)
}
return channelFlow {
val flows = currenciesIds.mapNotNull { currencyId ->
currencyId.rawCurrencyId?.let(dataStore::get)
}
return combine(flows) { quotes -> quotes.toSet() }
if (dataStore.isEmpty() || flows.isEmpty()) {
send(emptySet())
}
combine(flows) { quotes -> quotes.toSet() }.collect(::send)
}
}
override suspend fun store(response: QuotesResponse) {
response.quotes.forEach { (rawCurrencyId, quote) ->
dataStore.store(rawCurrencyId, StoredQuote(rawCurrencyId, quote))
val quotes = response.quotes.mapValues { (id, quote) ->
StoredQuote(id, quote)
}
dataStore.store(quotes)
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.datasource.local.quote
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.quote.model.StoredQuote
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import kotlinx.coroutines.flow.Flow
interface QuotesStore {

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.local.settings
import com.tangem.datasource.local.datastore.IntSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import kotlinx.coroutines.flow.Flow
interface AppLaunchCountStore {
fun get(): Flow<Int>
suspend fun getSyncOrNull(): Int?
suspend fun store(item: Int)
}
internal class DefaultAppLaunchCountStore(
store: IntSharedPreferencesDataStore,
) : AppLaunchCountStore, KeylessDataStoreDecorator<Int>(
wrappedDataStore = store,
key = "launchCount",
)

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.local.settings
import com.tangem.datasource.local.datastore.IntSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import kotlinx.coroutines.flow.Flow
interface AppRatingShowingCountStore {
fun get(): Flow<Int>
suspend fun getSyncOrNull(): Int?
suspend fun store(item: Int)
}
internal class DefaultAppRatingShowingCountStore(
store: IntSharedPreferencesDataStore,
) : AppRatingShowingCountStore, KeylessDataStoreDecorator<Int>(
wrappedDataStore = store,
key = "showRatingDialogAtLaunchCount",
)

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.local.settings
import com.tangem.datasource.local.datastore.LongSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import kotlinx.coroutines.flow.Flow
interface FundsFoundDateInMillisStore {
fun get(): Flow<Long>
suspend fun getSyncOrNull(): Long?
suspend fun store(item: Long)
}
internal class DefaultFundsFoundDateInMillisStore(
store: LongSharedPreferencesDataStore,
) : FundsFoundDateInMillisStore, KeylessDataStoreDecorator<Long>(
wrappedDataStore = store,
key = "fundsFoundDate",
)

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.local.settings
import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import kotlinx.coroutines.flow.Flow
interface UserInteractingStatusStore {
fun get(): Flow<Boolean>
suspend fun getSyncOrNull(): Boolean?
suspend fun store(item: Boolean)
}
internal class DefaultUserInteractingStatusStore(
store: BooleanSharedPreferencesDataStore,
) : UserInteractingStatusStore, KeylessDataStoreDecorator<Boolean>(
wrappedDataStore = store,
key = "userWasInteractWithRating",
)

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.wallets.models.UserWalletId
internal class DefaultUserMarketCoinsStore(
private val dataStore: StringKeyDataStore<CoinsResponse>,
) : UserMarketCoinsStore {
override suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse? {
return dataStore.getSyncOrNull(userWalletId.stringValue)
}
override suspend fun store(userWalletId: UserWalletId, item: CoinsResponse) {
dataStore.store(userWalletId.stringValue, item)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.domain.wallets.models.UserWalletId
interface UserMarketCoinsStore {
suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse?
suspend fun store(userWalletId: UserWalletId, item: CoinsResponse)
}

View file

@ -10,5 +10,5 @@ interface UserTokensStore {
suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse?
suspend fun store(key: UserWalletId, item: UserTokensResponse)
suspend fun store(key: UserWalletId, value: UserTokensResponse)
}

View file

@ -0,0 +1,42 @@
package com.tangem.datasource.local.txhistory
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.utils.extensions.addOrReplace
internal class DefaultTxHistoryItemsStore(
dataStore: StringKeyDataStore<Set<PaginationWrapper<TxHistoryItem>>>,
) : TxHistoryItemsStore,
StringKeyDataStoreDecorator<TxHistoryItemsStore.Key, Set<PaginationWrapper<TxHistoryItem>>>(dataStore) {
override fun provideStringKey(key: TxHistoryItemsStore.Key): String = key.toString()
override suspend fun getNextPageSyncOrNull(key: TxHistoryItemsStore.Key): Int? {
val storedValue = getSyncOrNull(key) ?: return null
val lastWrappedItems = storedValue.maxBy(PaginationWrapper<*>::page)
val lastPage = lastWrappedItems.page
return if (lastPage <= lastWrappedItems.totalPages) {
lastPage
} else {
null
}
}
override suspend fun getSyncOrNull(key: TxHistoryItemsStore.Key, page: Int): PaginationWrapper<TxHistoryItem>? {
val storedValue = getSyncOrNull(key)
return storedValue?.firstOrNull { it.page == page }
}
override suspend fun store(key: TxHistoryItemsStore.Key, value: PaginationWrapper<TxHistoryItem>) {
val oldValue = getSyncOrNull(key).orEmpty()
val newValue = oldValue.addOrReplace(value) {
it.page == value.page
}
store(key, newValue)
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.local.txhistory
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.wallets.models.UserWalletId
interface TxHistoryItemsStore {
suspend fun getNextPageSyncOrNull(key: Key): Int?
suspend fun getSyncOrNull(key: Key, page: Int): PaginationWrapper<TxHistoryItem>?
suspend fun remove(key: Key)
suspend fun store(key: Key, value: PaginationWrapper<TxHistoryItem>)
data class Key(
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
)
}

View file

@ -0,0 +1,24 @@
package com.tangem.datasource.local.userwallet
import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import kotlinx.coroutines.flow.Flow
/**
[REDACTED_AUTHOR]
*/
interface ShouldSaveUserWalletStore {
fun get(): Flow<Boolean>
suspend fun getSyncOrNull(): Boolean?
suspend fun store(item: Boolean)
}
internal class DefaultShouldSaveUserWalletStore(
store: BooleanSharedPreferencesDataStore,
) : ShouldSaveUserWalletStore, KeylessDataStoreDecorator<Boolean>(
wrappedDataStore = store,
key = "saveUserWallets",
)

View file

@ -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 kotlinx.coroutines.flow.Flow
internal class DefaultWalletManagersStore(
dataStore: StringKeyDataStore<List<WalletManager>>,
@ -15,6 +16,10 @@ internal class DefaultWalletManagersStore(
return key.stringValue
}
override fun getAll(userWalletId: UserWalletId): Flow<List<WalletManager>> {
return get(key = userWalletId)
}
override suspend fun getSyncOrNull(
userWalletId: UserWalletId,
blockchain: Blockchain,
@ -28,6 +33,10 @@ internal class DefaultWalletManagersStore(
}
}
override suspend fun getAllSync(userWalletId: UserWalletId): List<WalletManager> {
return getSyncOrNull(userWalletId) ?: emptyList()
}
override suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager) {
val walletManagers = getSyncOrNull(userWalletId)

View file

@ -3,15 +3,20 @@ package com.tangem.datasource.local.walletmanager
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
interface WalletManagersStore {
fun getAll(userWalletId: UserWalletId): Flow<List<WalletManager>>
suspend fun getSyncOrNull(
userWalletId: UserWalletId,
blockchain: Blockchain,
derivationPath: String?,
): WalletManager?
suspend fun getAllSync(userWalletId: UserWalletId): List<WalletManager>
suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager)
suspend fun clear()