Updated on 2026-08-14
This commit is contained in:
parent
6d56af68f5
commit
3d9e937ee9
30 changed files with 258 additions and 344 deletions
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.tap.network.auth
|
||||
|
||||
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.visa.model.getAuthHeader
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultVisaAuthProvider @Inject constructor(
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
) : TangemVisaAuthProvider {
|
||||
|
||||
override suspend fun getAuthHeader(cardId: String): String {
|
||||
val card = userWalletsListManager.userWalletsSync.firstOrNull { it.cardId == cardId }
|
||||
val status = card?.scanResponse?.visaCardActivationStatus as? VisaCardActivationStatus.Activated
|
||||
?: return "Error in the app!"
|
||||
return status.visaAuthTokens.getAuthHeader()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,17 @@
|
|||
package com.tangem.tap.network.auth.di
|
||||
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
import com.tangem.tap.network.auth.*
|
||||
import com.tangem.tap.network.auth.DefaultAppVersionProvider
|
||||
import com.tangem.tap.network.auth.DefaultAuthProvider
|
||||
import com.tangem.tap.network.auth.DefaultExpressAuthProvider
|
||||
import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider
|
||||
import com.tangem.tap.network.auth.DefaultVisaAuthProvider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -56,12 +52,3 @@ internal class AuthModule {
|
|||
return DefaultAppVersionProvider()
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface AuthBindModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindVisaAuthProvider(authStorage: DefaultVisaAuthProvider): TangemVisaAuthProvider
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.datasource.api.common.visa
|
||||
|
||||
interface TangemVisaAuthProvider {
|
||||
|
||||
suspend fun getAuthHeader(cardId: String): String
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.lib.visa.model
|
||||
package com.tangem.datasource.api.visa.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.visa.DefaultVisaRepository
|
||||
import com.tangem.data.visa.config.VisaLibLoader
|
||||
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
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ImplementedVisaDataModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
@ImplementedVisaRepository
|
||||
fun provideVisaRepository(impl: DefaultVisaRepository): VisaRepository
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import com.tangem.data.visa.config.VisaLibLoader
|
||||
import com.tangem.data.visa.converter.AccessCodeDataConverter
|
||||
import com.tangem.data.visa.converter.VisaActivationStatusConverter
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
|
|
@ -28,6 +29,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
private val visaAuthTokenStorage: VisaAuthTokenStorage,
|
||||
private val accessCodeDataConverter: AccessCodeDataConverter,
|
||||
private val visaAuthRepository: VisaAuthRepository,
|
||||
private val visaLibLoader: VisaLibLoader,
|
||||
) : VisaActivationRepository {
|
||||
|
||||
override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) {
|
||||
|
|
@ -192,6 +194,10 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getPinCodeRsaEncryptionPublicKey(): String {
|
||||
return visaLibLoader.getOrCreateConfig().rsaPublicKey
|
||||
}
|
||||
|
||||
private suspend fun <T : Any> request(requestBlock: suspend () -> T): T {
|
||||
return runCatching {
|
||||
requestBlock()
|
||||
|
|
|
|||
|
|
@ -12,21 +12,18 @@ 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.api.visa.TangemVisaApi
|
||||
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
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.model.VisaCurrency
|
||||
import com.tangem.domain.visa.model.VisaTxDetails
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
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
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -35,8 +32,8 @@ import kotlinx.coroutines.withContext
|
|||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Singleton
|
||||
internal class DefaultVisaRepository @Inject constructor(
|
||||
private val visaLibLoader: VisaLibLoader,
|
||||
|
|
@ -44,8 +41,8 @@ internal class DefaultVisaRepository @Inject constructor(
|
|||
private val cacheRegistry: CacheRegistry,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val visaAuthProvider: TangemVisaAuthProvider,
|
||||
private val visaAuthRepository: VisaAuthRepository,
|
||||
private val visaApiRequestMaker: VisaApiRequestMaker,
|
||||
private val visaApi: TangemVisaApi,
|
||||
) : VisaRepository {
|
||||
|
||||
private val currencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
|
|
@ -65,27 +62,32 @@ internal class DefaultVisaRepository @Inject constructor(
|
|||
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
|
||||
val address = makeAddress(userWalletId)
|
||||
|
||||
fetchVisaCurrencyIfExpired(address, isRefresh)
|
||||
fetchVisaCurrencyIfExpired(userWalletId, address, isRefresh)
|
||||
|
||||
return requireNotNull(fetchedCurrencies.value[address]) {
|
||||
"Unable to find VISA currency for $address"
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchVisaCurrencyIfExpired(address: String, isRefresh: Boolean) {
|
||||
private suspend fun fetchVisaCurrencyIfExpired(userWalletId: UserWalletId, address: String, isRefresh: Boolean) {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getVisaCurrencyKey(address),
|
||||
skipCache = isRefresh,
|
||||
block = { fetchVisaCurrency(address) },
|
||||
block = { fetchVisaCurrency(userWalletId, address) },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchVisaCurrency(address: String) {
|
||||
private suspend fun fetchVisaCurrency(userWalletId: UserWalletId, address: String) {
|
||||
val contractInfoProvider = visaLibLoader.getOrCreateProvider()
|
||||
|
||||
parZip(
|
||||
dispatchers.io,
|
||||
{ contractInfoProvider.getContractInfo(address) },
|
||||
{
|
||||
contractInfoProvider.getContractInfo(
|
||||
walletAddress = address,
|
||||
paymentAccountAddress = getPaymentAccountAddress(userWalletId),
|
||||
)
|
||||
},
|
||||
{ getFiatRate() },
|
||||
{ contractInfo, fiatRate ->
|
||||
fetchedCurrencies.update { value ->
|
||||
|
|
@ -104,7 +106,7 @@ internal class DefaultVisaRepository @Inject constructor(
|
|||
): Flow<PagingData<VisaTxHistoryItem>> {
|
||||
val userWallet = findVisaUserWallet(userWalletId)
|
||||
val cardPubKey = getCardPubKey(userWallet)
|
||||
val api = visaLibLoader.getOrCreateApi()
|
||||
|
||||
val pager = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = pageSize,
|
||||
|
|
@ -121,7 +123,7 @@ internal class DefaultVisaRepository @Inject constructor(
|
|||
cacheRegistry = cacheRegistry,
|
||||
fetchedItems = fetchedHistoryItems,
|
||||
dispatchers = dispatchers,
|
||||
requestTxHistory = { offset, pageSize -> getTxHistory(api, userWalletId, offset, pageSize) },
|
||||
requestTxHistory = { offset, pageSize -> getTxHistory(userWalletId, offset, pageSize) },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -145,22 +147,31 @@ internal class DefaultVisaRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getTxHistory(
|
||||
api: VisaApi,
|
||||
userWalletId: UserWalletId,
|
||||
offset: Int,
|
||||
pageSize: Int,
|
||||
): VisaTxHistoryResponse = withContext(dispatchers.io) {
|
||||
private suspend fun getPaymentAccountAddress(userWalletId: UserWalletId): String? = runCatching {
|
||||
val userWallet = findVisaUserWallet(userWalletId)
|
||||
val cardPubKey = getCardPubKey(userWallet)
|
||||
|
||||
request(userWalletId = userWalletId) {
|
||||
api.getTxHistory(
|
||||
authorizationHeader = visaAuthProvider.getAuthHeader(userWallet.cardId),
|
||||
cardPublicKey = cardPubKey,
|
||||
val customerInfo = visaApiRequestMaker.request(userWalletId) { authHeader, _ ->
|
||||
visaApi.getCustomerInfo(
|
||||
authHeader = authHeader,
|
||||
cardId = userWallet.scanResponse.card.cardId,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO select correct account when multiple accounts are available (will be implemented when backend is ready)
|
||||
customerInfo.paymentAccounts.firstOrNull()?.paymentAccountAddress
|
||||
}.getOrNull()
|
||||
|
||||
private suspend fun getTxHistory(userWalletId: UserWalletId, offset: Int, pageSize: Int): VisaTxHistoryResponse {
|
||||
return visaApiRequestMaker.request(
|
||||
userWalletId = userWalletId,
|
||||
) { authHeader, accessCodeData ->
|
||||
visaApi.getTxHistory(
|
||||
authHeader = authHeader,
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
limit = pageSize,
|
||||
offset = offset,
|
||||
).getOrThrow()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -217,56 +228,4 @@ internal class DefaultVisaRepository @Inject constructor(
|
|||
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")
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,10 @@ class MockVisaActivationRepository @AssistedInject constructor(
|
|||
|
||||
override suspend fun sendPinCode(pinCode: VisaEncryptedPinCode) {}
|
||||
|
||||
override suspend fun getPinCodeRsaEncryptionPublicKey(): String {
|
||||
return CryptoUtils.generateRandomBytes(length = 32).toHexString()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : VisaActivationRepository.Factory {
|
||||
override fun create(cardId: VisaCardId): MockVisaActivationRepository
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import com.tangem.domain.visa.model.VisaTxHistoryItem
|
|||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DummyVisaRepository : VisaRepository {
|
||||
internal class MockVisaRepository @Inject constructor() : VisaRepository {
|
||||
|
||||
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
|
||||
TODO("Not implemented for this build type")
|
||||
|
|
@ -11,6 +11,8 @@ internal data class VisaConfig(
|
|||
val mainnet: Addresses,
|
||||
@Json(name = "txHistoryAPIAdditionalHeaders")
|
||||
val header: Header,
|
||||
@Json(name = "rsaPublicKey")
|
||||
val rsaPublicKey: String,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
@ -1,13 +1,9 @@
|
|||
package com.tangem.data.visa.config
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.visa.BuildConfig
|
||||
import com.tangem.data.visa.utils.VisaConstants
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.lib.visa.VisaContractInfoProvider
|
||||
import com.tangem.lib.visa.api.VisaApi
|
||||
import com.tangem.lib.visa.api.VisaApiBuilder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
|
@ -15,21 +11,19 @@ import javax.inject.Inject
|
|||
|
||||
internal class VisaLibLoader @Inject constructor(
|
||||
private val assetLoader: AssetLoader,
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
private val createMutex = Mutex()
|
||||
private val createMutex2 = Mutex()
|
||||
|
||||
private var config: VisaConfig? = null
|
||||
|
||||
private var provider: VisaContractInfoProvider? = null
|
||||
private var api: VisaApi? = null
|
||||
|
||||
suspend fun getOrCreateConfig(): VisaConfig = config ?: getOrLoadConfig()
|
||||
|
||||
suspend fun getOrCreateProvider(): VisaContractInfoProvider = provider ?: createProvider()
|
||||
|
||||
suspend fun getOrCreateApi(): VisaApi = api ?: createApi()
|
||||
|
||||
private suspend fun createProvider(): VisaContractInfoProvider = createMutex.withLock {
|
||||
val config = getOrLoadConfig()
|
||||
|
||||
|
|
@ -54,24 +48,7 @@ internal class VisaLibLoader @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun createApi(): VisaApi = createMutex.withLock {
|
||||
val config = getOrLoadConfig()
|
||||
|
||||
api = VisaApiBuilder(
|
||||
useDevApi = VisaConstants.USE_TEST_ENV,
|
||||
isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
|
||||
moshi = moshi,
|
||||
headers = mapOf(
|
||||
X_ASN_HEADER_NAME to config.header.xAsn,
|
||||
),
|
||||
).build()
|
||||
|
||||
return requireNotNull(api) {
|
||||
"Visa API is not created"
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getOrLoadConfig(): VisaConfig {
|
||||
private suspend fun getOrLoadConfig(): VisaConfig = createMutex2.withLock {
|
||||
config = assetLoader.load<VisaConfig>(VISA_CONFIG_FILE_NAME)
|
||||
|
||||
return requireNotNull(config) {
|
||||
|
|
@ -81,6 +58,5 @@ internal class VisaLibLoader @Inject constructor(
|
|||
|
||||
companion object {
|
||||
private const val VISA_CONFIG_FILE_NAME = "tangem-app-config/visa_config"
|
||||
private const val X_ASN_HEADER_NAME = "x-asn"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
internal annotation class ImplementedVisaRepository
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import dagger.BindsOptionalOf
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ImplementedVisaRepositoryModule {
|
||||
|
||||
@BindsOptionalOf
|
||||
@ImplementedVisaRepository
|
||||
fun bindImplementedVisaRepository(): VisaRepository
|
||||
}
|
||||
|
|
@ -1,36 +1,20 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import com.tangem.data.visa.DefaultVisaAuthRepository
|
||||
import com.tangem.data.visa.DummyVisaRepository
|
||||
import com.tangem.data.visa.MockVisaRepository
|
||||
import com.tangem.data.visa.MockVisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.util.Optional
|
||||
import javax.inject.Singleton
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object VisaDataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVisaRepository(
|
||||
@ImplementedVisaRepository implementedVisaRepository: Optional<VisaRepository>,
|
||||
): VisaRepository {
|
||||
return implementedVisaRepository.getOrNull() ?: DummyVisaRepository()
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface VisaDataBindsModule {
|
||||
internal interface VisaDataModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
|
|
@ -48,4 +32,11 @@ internal interface VisaDataBindsModule {
|
|||
fun bindVisaActivationRepositoryFactory(
|
||||
repository: MockVisaActivationRepository.Factory,
|
||||
): VisaActivationRepository.Factory
|
||||
|
||||
// @Binds
|
||||
// fun bindVisaRepository(repository: DefaultVisaRepository): VisaRepository
|
||||
|
||||
// Mocked
|
||||
@Binds
|
||||
fun bindVisaRepository(repository: MockVisaRepository): VisaRepository
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.data.visa.converter.AccessCodeDataConverter
|
||||
import com.tangem.data.visa.model.AccessCodeData
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.visa.TangemVisaAuthApi
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.visa.model.getAuthHeader
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
typealias VisaAuthorizationHeader = String
|
||||
|
||||
internal class VisaApiRequestMaker @Inject constructor(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val visaAuthApi: TangemVisaAuthApi,
|
||||
private val accessCodeDataConverter: AccessCodeDataConverter,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
) {
|
||||
suspend fun <T : Any> request(
|
||||
userWalletId: UserWalletId,
|
||||
requestBlock: suspend (header: VisaAuthorizationHeader, accessCodeData: AccessCodeData) -> ApiResponse<T>,
|
||||
): T = withContext(dispatcherProvider.io) {
|
||||
val authTokens = getAuthTokens(userWalletId)
|
||||
val authHeader = authTokens.getAuthHeader()
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
runCatching {
|
||||
requestBlock(authHeader, accessCodeData).getOrThrow()
|
||||
}.getOrElse { responseError ->
|
||||
if (responseError !is ApiResponseError.HttpException ||
|
||||
responseError.code != ApiResponseError.HttpException.Code.UNAUTHORIZED
|
||||
) {
|
||||
throw responseError
|
||||
}
|
||||
|
||||
val newTokens = runCatching {
|
||||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val newAuthHeader = newTokens.getAuthHeader()
|
||||
val newAccessCodeData = accessCodeDataConverter.convert(newTokens)
|
||||
|
||||
requestBlock(newAuthHeader, newAccessCodeData).getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens {
|
||||
val result = visaAuthApi.refreshAccessToken(refreshToken.value).getOrThrow()
|
||||
|
||||
return VisaAuthTokens(
|
||||
accessToken = result.accessToken,
|
||||
refreshToken = VisaAuthTokens.RefreshToken(result.refreshToken),
|
||||
)
|
||||
}
|
||||
|
||||
@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")
|
||||
}
|
||||
|
||||
private suspend fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"No user wallet found: $userWalletId"
|
||||
}
|
||||
if (!userWallet.scanResponse.cardTypesResolver.isVisaWallet()) {
|
||||
error("VISA wallet required: $userWalletId")
|
||||
}
|
||||
|
||||
return userWallet
|
||||
}
|
||||
}
|
||||
|
|
@ -15,9 +15,9 @@ internal object VisaConstants {
|
|||
)
|
||||
|
||||
/*
|
||||
* Must be `false` in production
|
||||
* Don't forget to change CardTypesResolver.isVisaWallet
|
||||
* */
|
||||
* Must be `false` in production
|
||||
* Don't forget to change CardTypesResolver.isVisaWallet
|
||||
* */
|
||||
const val IS_DEMO_MODE_ENABLED = false
|
||||
|
||||
const val USE_TEST_ENV = true
|
||||
|
|
@ -40,5 +40,7 @@ internal fun getDemoAddress(): String {
|
|||
internal fun getDemoPublicKey(): String {
|
||||
return if (VisaConstants.USE_TEST_ENV) {
|
||||
VisaConstants.DEMO_TESTNET_PUBLIC_KEY
|
||||
} else VisaConstants.DEMO_MAINNET_PUBLIC_KEY
|
||||
} else {
|
||||
VisaConstants.DEMO_MAINNET_PUBLIC_KEY
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@ package com.tangem.data.visa.utils
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.externallinkprovider.TxExploreState
|
||||
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.VisaTxDetails
|
||||
import com.tangem.lib.visa.model.VisaTxHistoryResponse
|
||||
|
||||
internal class VisaTxDetailsFactory {
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
|
||||
import com.tangem.domain.visa.model.VisaTxHistoryItem
|
||||
import com.tangem.lib.visa.model.VisaTxHistoryResponse
|
||||
|
||||
internal class VisaTxHistoryItemFactory {
|
||||
|
||||
|
|
@ -3,13 +3,9 @@ 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.datasource.api.visa.models.response.VisaTxHistoryResponse
|
||||
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
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
|
@ -18,8 +18,6 @@ import javax.crypto.spec.SecretKeySpec
|
|||
|
||||
private const val KEY_SIZE = 256
|
||||
|
||||
private const val RSA_PUB_KEY = "TODO" // TODO add RSA public key [REDACTED_TASK_KEY]
|
||||
|
||||
class SetVisaPinCodeUseCase(
|
||||
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
|
||||
) {
|
||||
|
|
@ -30,9 +28,10 @@ class SetVisaPinCodeUseCase(
|
|||
pinCode: String,
|
||||
): Either<Throwable, Unit> = Either.catch {
|
||||
val visaActivationRepository = visaActivationRepositoryFactory.create(visaCardId)
|
||||
val rsaPublicKey = visaActivationRepository.getPinCodeRsaEncryptionPublicKey()
|
||||
|
||||
val sessionKey = generateSessionKey()
|
||||
val sessionId = getSessionId(RSA_PUB_KEY, sessionKey)
|
||||
val sessionId = getSessionId(rsaPublicKey, sessionKey)
|
||||
|
||||
val secureRandom = SecureRandom()
|
||||
val iv = ByteArray(size = 16)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ interface VisaActivationRepository {
|
|||
|
||||
suspend fun sendPinCode(pinCode: VisaEncryptedPinCode)
|
||||
|
||||
suspend fun getPinCodeRsaEncryptionPublicKey(): String
|
||||
|
||||
interface Factory {
|
||||
fun create(cardId: VisaCardId): VisaActivationRepository
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,27 +20,38 @@ internal class DefaultVisaContractInfoProvider(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : VisaContractInfoProvider {
|
||||
|
||||
override suspend fun getContractInfo(walletAddress: String): VisaContractInfo {
|
||||
override suspend fun getContractInfo(walletAddress: String, paymentAccountAddress: String?): VisaContractInfo {
|
||||
return parZip(
|
||||
dispatchers.io,
|
||||
{ loadPaymentAccount(walletAddress) },
|
||||
{ loadPaymentAccount(walletAddress = walletAddress, paymentAccountAddress = paymentAccountAddress) },
|
||||
{ loadPaymentTokenInfo() },
|
||||
{ paymentAccount, paymentToken ->
|
||||
fetchBalancesAndLimits(paymentAccount, paymentToken)
|
||||
fetchBalancesAndLimits(
|
||||
paymentAccount = paymentAccount,
|
||||
paymentToken = paymentToken,
|
||||
walletAddress = walletAddress,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadPaymentAccount(walletAddress: String): TangemPaymentAccount {
|
||||
private fun loadPaymentAccount(walletAddress: String, paymentAccountAddress: String?): TangemPaymentAccount {
|
||||
return TangemPaymentAccount.load(
|
||||
/* contractAddress = */ paymentAccountAddress ?: getPaymentAccountAddressFromRegistry(walletAddress),
|
||||
/* web3j = */ web3j,
|
||||
/* transactionManager = */ transactionManager,
|
||||
/* contractGasProvider = */ gasProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getPaymentAccountAddressFromRegistry(walletAddress: String): String {
|
||||
val paymentAccountRegistry = TangemPaymentAccountRegistry.load(
|
||||
/* contractAddress = */ paymentAccountRegistryAddress,
|
||||
/* web3j = */ web3j,
|
||||
/* transactionManager = */ transactionManager,
|
||||
/* contractGasProvider = */ gasProvider,
|
||||
)
|
||||
val paymentAccountAddress = paymentAccountRegistry.paymentAccountByCard(walletAddress).send()
|
||||
|
||||
return TangemPaymentAccount.load(paymentAccountAddress, web3j, transactionManager, gasProvider)
|
||||
return paymentAccountRegistry.paymentAccountByCard(walletAddress).send()
|
||||
}
|
||||
|
||||
private fun loadPaymentTokenInfo(): PaymentTokenInfo {
|
||||
|
|
@ -63,11 +74,12 @@ internal class DefaultVisaContractInfoProvider(
|
|||
private suspend fun fetchBalancesAndLimits(
|
||||
paymentAccount: TangemPaymentAccount,
|
||||
paymentToken: PaymentTokenInfo,
|
||||
walletAddress: String,
|
||||
): VisaContractInfo = parZip(
|
||||
dispatchers.io,
|
||||
{ fetchToken(paymentAccount) },
|
||||
{ fetchBalances(paymentAccount, paymentToken) },
|
||||
{ fetchLimits(paymentAccount, paymentToken) },
|
||||
{ fetchLimits(paymentAccount, paymentToken, walletAddress) },
|
||||
{ token, balances, (oldLimit, newLimit, changeDate) ->
|
||||
VisaContractInfo(token, balances, oldLimit, newLimit, changeDate)
|
||||
},
|
||||
|
|
@ -123,19 +135,15 @@ internal class DefaultVisaContractInfoProvider(
|
|||
private fun fetchLimits(
|
||||
paymentAccount: TangemPaymentAccount,
|
||||
paymentToken: PaymentTokenInfo,
|
||||
walletAddress: String,
|
||||
): Triple<Limits, Limits, Instant> {
|
||||
TODO() // TODO: [REDACTED_TASK_KEY]
|
||||
// val (
|
||||
// oldLimit,
|
||||
// newLimit,
|
||||
// changeDateSeconds,
|
||||
// ) = paymentAccount.cards("").send().component5()
|
||||
//
|
||||
// return Triple(
|
||||
// first = getLimits(oldLimit, paymentToken),
|
||||
// second = getLimits(newLimit, paymentToken),
|
||||
// third = changeDateSeconds.toInstant(),
|
||||
// )
|
||||
val limits = paymentAccount.cards(walletAddress).send().component5()
|
||||
|
||||
return Triple(
|
||||
first = getLimits(limits.oldValue, paymentToken),
|
||||
second = getLimits(limits.newValue, paymentToken),
|
||||
third = limits.changeTimestamp.toInstant(),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
|
|
|
|||
|
|
@ -21,7 +21,14 @@ import java.util.concurrent.TimeUnit
|
|||
|
||||
interface VisaContractInfoProvider {
|
||||
|
||||
suspend fun getContractInfo(walletAddress: String): VisaContractInfo
|
||||
/**
|
||||
* Fetches Visa contract info for the given wallet address.
|
||||
*
|
||||
* @param walletAddress Wallet address to fetch contract info for.
|
||||
* @param paymentAccountAddress Payment account address to fetch data from. If null,
|
||||
* it will be fetched from the registry.
|
||||
*/
|
||||
suspend fun getContractInfo(walletAddress: String, paymentAccountAddress: String?): VisaContractInfo
|
||||
|
||||
class Builder(
|
||||
private val useTestnetRpc: Boolean,
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
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,
|
||||
): ApiResponse<VisaTxHistoryResponse>
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
package com.tangem.lib.visa.api
|
||||
|
||||
import android.util.Log
|
||||
import com.ihsanbal.logging.Level
|
||||
import com.ihsanbal.logging.LoggingInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
|
||||
import com.tangem.lib.visa.utils.Constants
|
||||
import com.tangem.lib.visa.utils.Constants.NETWORK_LOGS_TAG
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class VisaApiBuilder(
|
||||
private val useDevApi: Boolean,
|
||||
private val isNetworkLoggingEnabled: Boolean,
|
||||
private val moshi: Moshi,
|
||||
private val headers: Map<String, String>,
|
||||
private val networkTimeoutSeconds: Long = Constants.NETWORK_TIMEOUT_SECONDS,
|
||||
) {
|
||||
|
||||
fun build(): VisaApi {
|
||||
val okHttpClient = createOkHttpClient()
|
||||
val retrofit = createRetrofit(okHttpClient)
|
||||
return retrofit.create(VisaApi::class.java)
|
||||
}
|
||||
|
||||
private fun createOkHttpClient(): OkHttpClient {
|
||||
val builder = OkHttpClient.Builder().apply {
|
||||
connectTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
|
||||
readTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
|
||||
writeTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
|
||||
|
||||
if (isNetworkLoggingEnabled) {
|
||||
addInterceptor(createNetworkLoggingInterceptor())
|
||||
}
|
||||
|
||||
if (headers.isNotEmpty()) {
|
||||
addInterceptor { chain ->
|
||||
val request = chain.request().newBuilder().apply {
|
||||
headers.forEach { (key, value) -> addHeader(key, value) }
|
||||
}.build()
|
||||
|
||||
chain.proceed(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun createRetrofit(okHttpClient: OkHttpClient): Retrofit {
|
||||
val baseUrl = if (useDevApi) Constants.VISA_API_DEV_URL else Constants.VISA_API_PROD_URL
|
||||
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
|
||||
.baseUrl(baseUrl)
|
||||
.client(okHttpClient)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNetworkLoggingInterceptor(): Interceptor {
|
||||
return LoggingInterceptor.Builder()
|
||||
.setLevel(Level.BODY)
|
||||
.log(Log.VERBOSE)
|
||||
.tag(NETWORK_LOGS_TAG)
|
||||
.build()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue