Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-10 16:15:30 +03:00
commit 2f62d482ca
635 changed files with 18802 additions and 9118 deletions

View file

@ -7,34 +7,51 @@ package com.tangem.datasource.api.common.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>()
/** Map of headers (header name with list of values) */
abstract val headers: Map<String, List<String>>
/**
* Represents an error response or failure from the API.
* Represents a successful response from the API
*
* @property cause The cause of the error.
* @property data the data returned by the API
* @property headers the headers returned by the API
*/
data class Error(val cause: ApiResponseError) : ApiResponse<Nothing>()
data class Success<T : Any>(
val data: T,
override val headers: Map<String, List<String>> = emptyMap(),
) : ApiResponse<T>()
/**
* Represents an error response or failure from the API
*
* @property cause the cause of the error
* @property headers the headers returned by the API
*/
data class Error(
val cause: ApiResponseError,
override val headers: Map<String, List<String>> = emptyMap(),
) : ApiResponse<Nothing>()
}
/**
* Wraps data in a [ApiResponse.Success] instance.
* Wraps data in a [ApiResponse.Success] instance
*
* @param data The data to wrap.
* @return A [ApiResponse.Success] instance containing the provided data.
* @param data the data to wrap
* @param headers the headers returned by the API
* @return a [ApiResponse.Success] instance containing the provided data
*/
internal fun <T : Any> apiSuccess(data: T): ApiResponse<T> = ApiResponse.Success(data)
internal fun <T : Any> apiSuccess(data: T, headers: Map<String, List<String>>): ApiResponse<T> {
return ApiResponse.Success(data, headers)
}
/**
* Wraps an [ApiResponseError] in a [ApiResponse.Error] instance.
* Wraps an [ApiResponseError] in a [ApiResponse.Error] instance
*
* @param cause The error to wrap.
* @return A [ApiResponse.Error] instance containing the provided error.
* @param cause the error to wrap
* @param headers the headers returned by the API
* @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>
internal fun <T : Any> apiError(cause: ApiResponseError, headers: Map<String, List<String>>): ApiResponse<T> {
return ApiResponse.Error(cause, headers) as ApiResponse<T>
}

View file

@ -47,7 +47,8 @@ internal class ApiResponseCallDelegate<T : Any>(
Timber.e(e, "onFailure UnknownException")
ApiResponseError.UnknownException(e)
}
val safeResponse = apiError<T>(error)
val safeResponse = apiError<T>(cause = error, headers = emptyMap())
responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse))
}

View file

@ -20,6 +20,8 @@ sealed class ApiResponseError : Exception() {
// region Error Codes
enum class Code(val numericCode: Int) {
// 3xx Server Errors
NOT_MODIFIED(numericCode = 304),
// 4xx Server Errors
BAD_REQUEST(numericCode = 400),
UNAUTHORIZED(numericCode = 401),
@ -64,10 +66,6 @@ sealed class ApiResponseError : Exception() {
;
override fun toString(): String = "$numericCode - $name"
companion object {
val values = values()
}
}
// endregion Error Codes
}

View file

@ -19,4 +19,9 @@ inline fun <T> catchApiResponseError(onError: (ApiResponseError) -> Unit, block:
onError(e)
throw e
}
}
/** Checks if the ApiResponseError is a network error with the specified HTTP status [code] */
fun ApiResponseError.isNetworkError(code: ApiResponseError.HttpException.Code): Boolean {
return this is ApiResponseError.HttpException && this.code == code
}

View file

@ -0,0 +1,3 @@
package com.tangem.datasource.api.common.response
const val IF_NONE_MATCH_HEADER = "IfNoneMatch"

View file

@ -12,12 +12,13 @@ import java.util.concurrent.TimeoutException
import javax.net.ssl.SSLHandshakeException
internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: AnalyticsErrorHandler): ApiResponse<T> {
val headers = headers().toMultimap()
val body = body()
return if (isSuccessful && body != null) {
apiSuccess(body)
apiSuccess(data = body, headers = headers)
} else {
val code = ApiResponseError.HttpException.Code.values
val code = ApiResponseError.HttpException.Code.entries
.firstOrNull { it.numericCode == code() }
val e = try {
if (code == null) {
@ -33,7 +34,7 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: Anal
ApiResponseError.UnknownException(e)
}
apiError(e)
apiError(e, headers)
}
}

View file

@ -103,4 +103,7 @@ interface TangemPayApi {
@GET("v1/customer/kyc")
suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse<KycAccessInfoResponse>
@GET("v1/customer/me")
suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse<CustomerMeResponse>
}

View file

@ -0,0 +1,48 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CustomerMeResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "id") val id: String,
@Json(name = "state") val state: String,
@Json(name = "createdAt") val createdAt: String,
@Json(name = "product_instance") val productInstance: ProductInstance,
@Json(name = "payment_account") val paymentAccount: PaymentAccount,
@Json(name = "kyc") val kyc: Kyc,
)
@JsonClass(generateAdapter = true)
data class ProductInstance(
@Json(name = "id") val id: String,
@Json(name = "cid") val cid: String,
@Json(name = "card_id") val cardId: String,
@Json(name = "card_wallet_address") val cardWalletAddress: String,
@Json(name = "status") val status: String,
@Json(name = "updated_at") val updatedAt: String,
@Json(name = "payment_account_id") val paymentAccountId: String,
)
@JsonClass(generateAdapter = true)
data class PaymentAccount(
@Json(name = "id") val id: String,
@Json(name = "address") val address: String,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
)
@JsonClass(generateAdapter = true)
data class Kyc(
@Json(name = "id") val id: String,
@Json(name = "provider") val provider: String,
@Json(name = "status") val status: String,
@Json(name = "risk") val risk: String,
@Json(name = "review_answer") val reviewAnswer: String,
@Json(name = "created_at") val createdAt: String,
)
}

View file

@ -154,7 +154,8 @@ interface TangemTechApi {
suspend fun saveWalletAccounts(
@Path("walletId") walletId: String,
@Header("If-Match") ifMatch: String,
): ApiResponse<SaveWalletAccountsResponse>
@Body body: SaveWalletAccountsResponse,
): ApiResponse<Unit>
@GET("/v1/wallets/{walletId}/accounts/archived")
suspend fun getWalletArchivedAccounts(

View file

@ -15,7 +15,7 @@ data class GetWalletAccountsResponse(
@JsonClass(generateAdapter = true)
data class Wallet(
@Json(name = "version") val version: Int,
@Json(name = "version") val version: Int = 0,
@Json(name = "group") val group: GroupType,
@Json(name = "sort") val sort: SortType,
@Json(name = "totalAccounts") val totalAccounts: Int,

View file

@ -14,11 +14,18 @@ interface RuntimeStateStore<T> {
/** Get flow of elements [T] */
fun get(): StateFlow<T>
/** Get element [T] synchronously or null */
suspend fun getSyncOrNull(): T?
/** Store [value] */
suspend fun store(value: T)
/** Update current value by [function] */
suspend fun update(function: (T) -> T)
/** Clear stored value */
fun clear()
companion object {
/**
@ -32,6 +39,8 @@ interface RuntimeStateStore<T> {
override fun get(): StateFlow<T> = flow
override suspend fun getSyncOrNull(): T? = flow.value
override suspend fun store(value: T) {
flow.value = value
}
@ -39,6 +48,10 @@ interface RuntimeStateStore<T> {
override suspend fun update(function: (T) -> T) {
flow.update(function)
}
override fun clear() {
flow.value = defaultValue
}
}
}
}

View file

@ -7,7 +7,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TANGEM_TOS_ACC
import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_ASK_BIOMETRY_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY
@ -26,7 +26,7 @@ object PreferencesKeys {
val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") }
val SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") }
val SHOULD_SHOW_ASK_BIOMETRY_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") }
val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") }
@ -56,10 +56,6 @@ object PreferencesKeys {
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringPreferencesKey(name = "excludedBlockchainsV2") }
val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") }
val IS_TANGEM_TOS_ACCEPTED_KEY by lazy { booleanPreferencesKey(name = "tangem_tos_accepted") }
@ -187,7 +183,7 @@ object PreferencesKeys {
internal fun getTapPrefKeysToMigrate(): Set<String> {
return setOf(
SAVE_USER_WALLETS_KEY,
SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY,
SHOULD_SHOW_ASK_BIOMETRY_KEY,
APP_LAUNCH_COUNT_KEY,
SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY,
FUNDS_FOUND_DATE_KEY,

View file

@ -39,5 +39,13 @@ internal class DefaultUserTokensResponseStore(
)
}
override suspend fun clear(userWalletId: UserWalletId) {
appPreferencesStore.updateData { preferences ->
val key = createPreferencesKey(userWalletId = userWalletId.stringValue)
preferences.toMutablePreferences().apply { remove(key) }
}
}
private fun createPreferencesKey(userWalletId: String) = stringPreferencesKey(name = "user_tokens_$userWalletId")
}

View file

@ -17,4 +17,6 @@ interface UserTokensResponseStore {
suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse?
suspend fun store(userWalletId: UserWalletId, response: UserTokensResponse)
suspend fun clear(userWalletId: UserWalletId)
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.local.visa
interface TangemPayStorage {
suspend fun store(authHeader: String)
suspend fun get(): String?
suspend fun clear()
}