diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt index 36aa8bf2de..921fce0ccc 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt @@ -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" } } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index e558b2def0..f3211c1994 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -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 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") + } } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt index 60d487838e..efb081bdc6 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt @@ -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 } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt index 4af82f3014..e233435e55 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt @@ -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>>, private val dispatchers: CoroutineDispatcherProvider, + val requestTxHistory: suspend (offset: Int, pageSize: Int) -> VisaTxHistoryResponse, ) : PagingSource() { 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, diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt index a4646bf7b6..5174396612 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -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"), ) } } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt index 0341e9ad8a..7131b2533f 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt @@ -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, -) \ No newline at end of file + @Json(name = "refreshToken") val refreshToken: RefreshToken, +) { + + @Serializable + @JvmInline + value class RefreshToken(val value: String) +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt index 7e1a10a1c8..dc9e44c563 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt @@ -27,6 +27,9 @@ sealed class VisaCardActivationStatus { @Serializable data object Blocked : VisaCardActivationStatus() + @Serializable + data object RefreshTokenExpired : VisaCardActivationStatus() + companion object { val jsonAdapter: PolymorphicJsonAdapterFactory get() = PolymorphicJsonAdapterFactory.of(VisaCardActivationStatus::class.java, "type") diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/exception/RefreshTokenExpiredException.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/exception/RefreshTokenExpiredException.kt new file mode 100644 index 0000000000..a125157184 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/exception/RefreshTokenExpiredException.kt @@ -0,0 +1,3 @@ +package com.tangem.domain.visa.exception + +class RefreshTokenExpiredException : Exception("Visa: refresh token expired") \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt index a280b47b2c..1182e20b73 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt @@ -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 } \ No newline at end of file diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApi.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApi.kt index b05b3096a2..106ced7a60 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApi.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApi.kt @@ -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,