Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-12 11:33:24 +03:00
commit 8148f32272
642 changed files with 10717 additions and 5789 deletions

View file

@ -1,6 +0,0 @@
package com.tangem.datasource.api.common.visa
interface TangemVisaAuthProvider {
suspend fun getAuthHeader(cardId: String): String
}

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.express.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
@JsonClass(generateAdapter = true)
data class ExchangeStatusResponse(
@ -26,6 +27,12 @@ data class ExchangeStatusResponse(
@Json(name = "refundContractAddress")
val refundContractAddress: String? = null,
@Json(name = "createdAt")
val createdAt: DateTime? = null,
@Json(name = "averageDuration")
val averageDuration: Int? = null,
)
@JsonClass(generateAdapter = false)

View file

@ -5,9 +5,7 @@ import com.tangem.datasource.api.utils.ReadTimeout
import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest
import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse
import com.tangem.datasource.api.visa.models.response.CardWalletDataToSignResponse
import com.tangem.datasource.api.visa.models.response.CustomerWalletDataToSignResponse
import com.tangem.datasource.api.visa.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
@ -71,4 +69,19 @@ interface TangemVisaApi {
@Header("Authorization") authHeader: String,
@Body body: SetPinCodeRequest,
): ApiResponse<Unit>
@GET("customer/info")
suspend fun getCustomerInfo(
@Header("Authorization") authHeader: String,
@Query("card_id") cardId: String,
): ApiResponse<VisaCustomerInfo>
@GET("product_instance/transactions")
suspend fun getTxHistory(
@Header("Authorization") authHeader: String,
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("limit") limit: Int,
@Query("offset") offset: Int,
): ApiResponse<VisaTxHistoryResponse>
}

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.api.visa
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse
import com.tangem.datasource.api.visa.models.response.JWTResponse
import retrofit2.http.Field
@ -27,5 +28,5 @@ interface TangemVisaAuthApi {
): JWTResponse
@POST("auth/refresh_token")
suspend fun refreshAccessToken(@Field("refresh_token") refreshToken: String): JWTResponse
suspend fun refreshAccessToken(@Field("refresh_token") refreshToken: String): ApiResponse<JWTResponse>
}

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class VisaCustomerInfo(
@Json(name = "payment_accounts") val paymentAccounts: List<PaymentAccount>,
) {
data class PaymentAccount(
@Json(name = "id") val id: String,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
@Json(name = "payment_account_address") val paymentAccountAddress: String,
)
}

View file

@ -0,0 +1,88 @@
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class VisaTxHistoryResponse(
@Json(name = "card_wallet_address")
val cardWalletAddress: String,
@Json(name = "transactions")
val transactions: List<Transaction>,
) {
@JsonClass(generateAdapter = true)
data class Transaction(
@Json(name = "auth_code")
val authCode: String?,
@Json(name = "billing_amount")
val billingAmount: BigDecimal,
@Json(name = "billing_currency_code")
val billingCurrencyCode: Int,
@Json(name = "blockchain_amount")
val blockchainAmount: BigDecimal,
@Json(name = "blockchain_coin_name")
val blockchainCoinName: String,
@Json(name = "blockchain_fee")
val blockchainFee: BigDecimal,
// @Json(name = "local_dt")
// val localDate: DateTime?,
@Json(name = "merchant_category_code")
val merchantCategoryCode: String?,
@Json(name = "merchant_city")
val merchantCity: String?,
@Json(name = "merchant_country_code")
val merchantCountryCode: String?,
@Json(name = "merchant_name")
val merchantName: String?,
@Json(name = "requests")
val requests: List<Request>,
@Json(name = "rrn")
val rrn: String?,
@Json(name = "transaction_amount")
val transactionAmount: BigDecimal,
@Json(name = "transaction_currency_code")
val transactionCurrencyCode: Int,
@Json(name = "transaction_dt")
val transactionDt: DateTime,
@Json(name = "transaction_id")
val transactionId: Long,
@Json(name = "transaction_status")
val transactionStatus: String,
@Json(name = "transaction_type")
val transactionType: String,
) {
@JsonClass(generateAdapter = true)
data class Request(
@Json(name = "billing_amount")
val billingAmount: BigDecimal,
@Json(name = "billing_currency_code")
val billingCurrencyCode: Int,
@Json(name = "blockchain_amount")
val blockchainAmount: BigDecimal,
@Json(name = "blockchain_fee")
val blockchainFee: BigDecimal,
@Json(name = "error_code")
val errorCode: Int,
@Json(name = "request_dt")
val requestDt: DateTime,
@Json(name = "request_status")
val requestStatus: String,
@Json(name = "request_type")
val requestType: String,
@Json(name = "transaction_amount")
val transactionAmount: BigDecimal,
@Json(name = "transaction_currency_code")
val transactionCurrencyCode: Int,
@Json(name = "transaction_request_id")
val transactionRequestId: Long,
@Json(name = "tx_hash")
val txHash: String?,
@Json(name = "tx_status")
val txStatus: String?,
)
}
}

View file

@ -1,12 +1,23 @@
package com.tangem.datasource.di
import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.token.DefaultExpressAssetsStore
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@ -15,7 +26,22 @@ internal object ExpressAssetsStoreModule {
@Provides
@Singleton
fun provideExpressAssetsStore(): ExpressAssetsStore {
return DefaultExpressAssetsStore(dataStore = RuntimeDataStore())
fun provideExpressAssetsStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): ExpressAssetsStore {
return DefaultExpressAssetsStore(
persistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithStringKeyTypes<Asset>(),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile("express_assets") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
),
runtimeStore = RuntimeDataStore(),
)
}
}

View file

@ -12,7 +12,9 @@ import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -41,8 +43,6 @@ internal class DefaultExpressServiceLoader @Inject constructor(
withContext(dispatchers.io) {
val initializationStatus = getInitializationStatusInternal(userWalletId)
initializationStatus.update { lceLoading() }
try {
if (userTokens.isNotEmpty()) {
val response = tangemExpressApi.getAssets(
@ -54,22 +54,24 @@ internal class DefaultExpressServiceLoader @Inject constructor(
initializationStatus.update { response.lceContent() }
}
} catch (e: Throwable) {
initializationStatus.update { e.lceError() }
if (expressAssetsStore.getSyncOrNull(userWalletId) == null) {
initializationStatus.update { e.lceError() }
}
Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}")
}
}
}
override fun getInitializationStatus(userWalletId: UserWalletId): InitializationStatusFlow {
return getInitializationStatusInternal(userWalletId)
override fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<Asset>>> {
return flow { getInitializationStatusInternal(userWalletId).collect { emit(it) } }
}
private fun getInitializationStatusInternal(userWalletId: UserWalletId): InitializationStatusFlow {
private suspend fun getInitializationStatusInternal(userWalletId: UserWalletId): InitializationStatusFlow {
val initializationStatus = initializationStatuses.value.get(key = userWalletId)
if (initializationStatus != null) return initializationStatus
val default: InitializationStatusFlow = MutableStateFlow(value = lceLoading())
val cached = expressAssetsStore.getSyncOrNull(userWalletId)
val default: InitializationStatusFlow = MutableStateFlow(value = cached?.lceContent() ?: lceLoading())
initializationStatuses.update {
it.toMutableMap().apply {

View file

@ -4,7 +4,7 @@ import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.Flow
/**
* Express service loader
@ -17,5 +17,5 @@ interface ExpressServiceLoader {
suspend fun update(userWalletId: UserWalletId, userTokens: List<LeastTokenInfo>)
/** Get initialization status by [userWalletId] */
fun getInitializationStatus(userWalletId: UserWalletId): StateFlow<Lce<Throwable, List<Asset>>>
fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<Asset>>>
}

View file

@ -121,6 +121,8 @@ object PreferencesKeys {
val SEED_FIRST_NOTIFICATION_SHOW_TIME by lazy { longPreferencesKey("seedFirstNotificationTime") }
val WALLETS_NFT_ENABLED_STATES_KEY by lazy { stringPreferencesKey(name = "walletsNftEnabledStates") }
fun getShouldShowStoriesKey(storyId: String) = booleanPreferencesKey("shouldShowStories_$storyId")
// region Permission

View file

@ -1,18 +1,53 @@
package com.tangem.datasource.local.token
import androidx.datastore.core.DataStore
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
internal typealias AssetsByWalletId = Map<String, List<Asset>>
internal class DefaultExpressAssetsStore(
private val dataStore: StringKeyDataStore<List<Asset>>,
private val persistenceStore: DataStore<AssetsByWalletId>,
private val runtimeStore: StringKeyDataStore<List<Asset>>,
) : ExpressAssetsStore {
override suspend fun getSyncOrNull(userWalletId: UserWalletId): List<Asset>? {
return dataStore.getSyncOrNull(userWalletId.stringValue)
val runtimeAssets = runtimeStore.getSyncOrNull(userWalletId.stringValue)
if (runtimeAssets != null) {
return runtimeAssets
}
val cachedAssets = getCachedAssets(userWalletId)
return if (cachedAssets != null) {
runtimeStore.store(userWalletId.stringValue, cachedAssets)
cachedAssets
} else {
null
}
}
private suspend fun getCachedAssets(userWalletId: UserWalletId): List<Asset>? {
return persistenceStore.data.firstOrNull().orEmpty()[userWalletId.stringValue]
}
override suspend fun store(userWalletId: UserWalletId, item: List<Asset>) {
dataStore.store(userWalletId.stringValue, item)
coroutineScope {
launch {
runtimeStore.store(userWalletId.stringValue, item)
}
launch {
persistenceStore.updateData {
it.toMutableMap().apply {
put(userWalletId.stringValue, item)
}
}
}
}
}
}