Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-29 16:55:54 +03:00
parent d8348757d2
commit 2d67f5a8fd
10 changed files with 135 additions and 35 deletions

View file

@ -1,14 +1,20 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import javax.inject.Inject
internal class DefaultVisaAuthProvider @Inject constructor(
private val authStorage: VisaAuthTokenStorage,
private val userWalletsListManager: UserWalletsListManager,
) : TangemVisaAuthProvider {
override suspend fun getAuthHeader(cardId: String): String {
return authStorage.get(cardId)?.accessToken?.let { "Bearer $it" } ?: "Error in the app!"
val card = userWalletsListManager.userWalletsSync.firstOrNull { it.cardId == cardId }
val status = card?.scanResponse?.visaCardActivationStatus as? VisaCardActivationStatus.Activated
?: return "Error in the app!"
val accessToken = status.visaAuthTokens.accessToken
return "Bearer $accessToken"
}
}

View file

@ -6,22 +6,25 @@ import androidx.paging.PagingData
import arrow.fx.coroutines.parZip
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.visa.config.VisaLibLoader
import com.tangem.data.visa.utils.*
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.visa.repository.VisaRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.lib.visa.api.VisaApi
import com.tangem.lib.visa.model.VisaTxHistoryResponse
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
@ -29,13 +32,19 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.jvm.Throws
internal class DefaultVisaRepository(
@Singleton
internal class DefaultVisaRepository @Inject constructor(
private val visaLibLoader: VisaLibLoader,
private val tangemTechApi: TangemTechApi,
private val cacheRegistry: CacheRegistry,
private val userWalletsStore: UserWalletsStore,
private val dispatchers: CoroutineDispatcherProvider,
private val visaAuthProvider: TangemVisaAuthProvider,
private val visaAuthRepository: VisaAuthRepository,
) : VisaRepository {
private val currencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -106,11 +115,12 @@ internal class DefaultVisaRepository(
cardPublicKey = cardPubKey,
pageSize = pageSize,
isRefresh = isRefresh,
userWallet = userWallet,
),
cacheRegistry = cacheRegistry,
visaApi = api,
fetchedItems = fetchedHistoryItems,
dispatchers = dispatchers,
requestTxHistory = { offset, pageSize -> getTxHistory(api, userWalletId, offset, pageSize) },
)
},
)
@ -134,6 +144,25 @@ internal class DefaultVisaRepository(
}
}
private suspend fun getTxHistory(
api: VisaApi,
userWalletId: UserWalletId,
offset: Int,
pageSize: Int,
): VisaTxHistoryResponse = withContext(dispatchers.io) {
val userWallet = findVisaUserWallet(userWalletId)
val cardPubKey = getCardPubKey(userWallet)
request(userWalletId = userWalletId) {
api.getTxHistory(
authorizationHeader = visaAuthProvider.getAuthHeader(userWallet.cardId),
cardPublicKey = cardPubKey,
limit = pageSize,
offset = offset,
).getOrThrow()
}
}
private suspend fun makeAddress(userWalletId: UserWalletId): String {
if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoAddress()
@ -166,9 +195,9 @@ internal class DefaultVisaRepository(
if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoPublicKey()
val cardWallet = userWallet.scanResponse.card.wallets.firstOrNull {
it.curve == EllipticCurve.Secp256k1
it.curve == VisaUtilities.mandatoryCurve
}
requireNotNull(cardWallet) { "Secp256k1 card wallet not found" }
requireNotNull(cardWallet) { "Visa card wallet not found" }
return cardWallet.publicKey.toHexString()
}
@ -187,4 +216,56 @@ internal class DefaultVisaRepository(
private fun getVisaCurrencyKey(address: String): String {
return "visa_currency_$address"
}
private suspend fun <T : Any> request(
userWalletId: UserWalletId,
requestBlock: suspend () -> T,
): T {
return runCatching {
requestBlock()
}.getOrElse { responseError ->
if (responseError !is ApiResponseError.HttpException ||
responseError.code != ApiResponseError.HttpException.Code.UNAUTHORIZED
) {
throw responseError
}
val authTokens = getAuthTokens(userWalletId)
val newTokens = runCatching {
visaAuthRepository.refreshAccessTokens(authTokens.refreshToken)
}.getOrElse {
if (it is ApiResponseError.HttpException &&
it.code == ApiResponseError.HttpException.Code.UNAUTHORIZED
) {
userWalletsStore.update(userWalletId) { userWallet ->
userWallet.copy(
scanResponse = userWallet.scanResponse.copy(
visaCardActivationStatus = VisaCardActivationStatus.RefreshTokenExpired
)
)
}
}
throw RefreshTokenExpiredException()
}
userWalletsStore.update(userWalletId) { userWallet ->
userWallet.copy(
scanResponse = userWallet.scanResponse.copy(
visaCardActivationStatus = VisaCardActivationStatus.Activated(
visaAuthTokens = newTokens
)
)
)
}
requestBlock()
}
}
@Throws
private suspend fun getAuthTokens(userWalletId: UserWalletId): VisaAuthTokens {
val userWallet = findVisaUserWallet(userWalletId)
val status = userWallet.scanResponse.visaCardActivationStatus ?: error("Visa card activation status not found")
return (status as? VisaCardActivationStatus.Activated)?.visaAuthTokens ?: error("Visa card is not activated")
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.visa.repository.VisaRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -15,22 +16,10 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object ImplementedVisaDataModule {
internal interface ImplementedVisaDataModule {
@Provides
@Binds
@Singleton
@ImplementedVisaRepository
fun provideVisaRepository(
visaLibLoader: VisaLibLoader,
tangemTechApi: TangemTechApi,
cacheRegistry: CacheRegistry,
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
): VisaRepository = DefaultVisaRepository(
visaLibLoader,
tangemTechApi,
cacheRegistry,
userWalletsStore,
dispatchers,
)
fun provideVisaRepository(impl: DefaultVisaRepository): VisaRepository
}

View file

@ -3,8 +3,11 @@ package com.tangem.data.visa.utils
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.lib.visa.api.VisaApi
import com.tangem.lib.visa.model.VisaTxHistoryResponse
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -16,9 +19,9 @@ import timber.log.Timber
internal class VisaTxHistoryPagingSource(
params: Params,
private val cacheRegistry: CacheRegistry,
private val visaApi: VisaApi,
private val fetchedItems: MutableStateFlow<Map<String, List<VisaTxHistoryResponse.Transaction>>>,
private val dispatchers: CoroutineDispatcherProvider,
val requestTxHistory: suspend (offset: Int, pageSize: Int) -> VisaTxHistoryResponse,
) : PagingSource<Int, VisaTxHistoryItem>() {
private val itemsFactory = VisaTxHistoryItemFactory()
@ -73,11 +76,7 @@ internal class VisaTxHistoryPagingSource(
}
private suspend fun fetchItems(offset: Int, pageSize: Int) = withContext(dispatchers.io) {
val response = visaApi.getTxHistory(
cardPublicKey = cardPublicKey,
limit = pageSize,
offset = offset,
).getOrThrow()
val response = requestTxHistory(offset, pageSize)
fetchedItems.update {
it.toMutableMap().apply {
@ -97,6 +96,7 @@ internal class VisaTxHistoryPagingSource(
}
class Params(
val userWallet: UserWallet,
val cardPublicKey: String,
val pageSize: Int,
val isRefresh: Boolean,

View file

@ -80,7 +80,16 @@ internal class DefaultVisaAuthRepository @Inject constructor(
// )
VisaAuthTokens(
accessToken = "accessToken",
refreshToken = "refreshToken",
refreshToken = VisaAuthTokens.RefreshToken("refreshToken"),
)
}
override suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens =
withContext(dispatchers.io) {
// TODO
VisaAuthTokens(
accessToken = "accessToken",
refreshToken = VisaAuthTokens.RefreshToken("new refreshToken"),
)
}
}

View file

@ -8,5 +8,10 @@ import kotlinx.serialization.Serializable
@JsonClass(generateAdapter = true)
data class VisaAuthTokens(
@Json(name = "accessToken") val accessToken: String,
@Json(name = "refreshToken") val refreshToken: String,
)
@Json(name = "refreshToken") val refreshToken: RefreshToken,
) {
@Serializable
@JvmInline
value class RefreshToken(val value: String)
}

View file

@ -27,6 +27,9 @@ sealed class VisaCardActivationStatus {
@Serializable
data object Blocked : VisaCardActivationStatus()
@Serializable
data object RefreshTokenExpired : VisaCardActivationStatus()
companion object {
val jsonAdapter: PolymorphicJsonAdapterFactory<VisaCardActivationStatus>
get() = PolymorphicJsonAdapterFactory.of(VisaCardActivationStatus::class.java, "type")

View file

@ -0,0 +1,3 @@
package com.tangem.domain.visa.exception
class RefreshTokenExpiredException : Exception("Visa: refresh token expired")

View file

@ -11,4 +11,6 @@ interface VisaAuthRepository {
suspend fun getCustomerWalletAuthChallenge(cardId: String, walletPublicKey: String): VisaAuthChallenge.Wallet
suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens
suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens
}

View file

@ -3,12 +3,14 @@ package com.tangem.lib.visa.api
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.lib.visa.model.VisaTxHistoryResponse
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface VisaApi {
@GET("transaction")
suspend fun getTxHistory(
@Header("Authorization") authorizationHeader: String,
@Query("card_public_key") cardPublicKey: String,
@Query("limit") limit: Int,
@Query("offset") offset: Int,