Updated on 2026-08-14
This commit is contained in:
parent
d8348757d2
commit
2d67f5a8fd
10 changed files with 135 additions and 35 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.domain.visa.exception
|
||||
|
||||
class RefreshTokenExpiredException : Exception("Visa: refresh token expired")
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue