Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-26 13:52:26 +00:00
commit 1d5e9692cf
382 changed files with 6038 additions and 4437 deletions

View file

@ -12,6 +12,7 @@ android {
dependencies {
implementation(deps.androidx.datastore)
implementation(deps.androidx.appCompat)
/** DI */
implementation(deps.hilt.android)

View file

@ -8,8 +8,11 @@ import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import javax.inject.Singleton
internal class DefaultBalanceHidingRepository(
@Singleton
internal class DefaultBalanceHidingRepository @Inject constructor(
private val appPreferencesStore: AppPreferencesStore,
) : BalanceHidingRepository {

View file

@ -3,18 +3,40 @@ package com.tangem.data.balancehiding
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorManager
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.tangem.domain.balancehiding.DeviceFlipDetector
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Singleton
internal class DefaultDeviceFlipDetector(context: Context) : DeviceFlipDetector {
@Singleton
class DefaultDeviceFlipDetector @Inject constructor(
@ApplicationContext context: Context,
) : DeviceFlipDetector, DefaultLifecycleObserver {
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private var gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
private var isResumedState = AtomicBoolean(false)
override fun onPause(owner: LifecycleOwner) {
isResumedState.set(false)
}
override fun onResume(owner: LifecycleOwner) {
isResumedState.set(true)
}
override fun getDeviceFlipFlow(): Flow<Unit> = callbackFlow {
val listener = FlipListener { trySend(Unit) }
val listener = FlipListener {
if (isResumedState.get()) {
trySend(Unit)
}
}
gravitySensor?.let {
sensorManager.registerListener(listener, it, SensorManager.SENSOR_DELAY_NORMAL)

View file

@ -1,31 +1,24 @@
package com.tangem.data.balancehiding.di
import android.content.Context
import com.tangem.data.balancehiding.DefaultBalanceHidingRepository
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.balancehiding.DeviceFlipDetector
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object BalanceHidingModule {
internal interface BalanceHidingModule {
@Provides
@Binds
@Singleton
fun provideBalanceHidingRepository(appPreferencesStore: AppPreferencesStore): BalanceHidingRepository {
return DefaultBalanceHidingRepository(appPreferencesStore = appPreferencesStore)
}
fun provideBalanceHidingRepository(impl: DefaultBalanceHidingRepository): BalanceHidingRepository
@Provides
@Binds
@Singleton
fun provideFlipDetector(@ApplicationContext context: Context): DeviceFlipDetector {
return DefaultDeviceFlipDetector(context = context)
}
fun provideFlipDetector(impl: DefaultDeviceFlipDetector): DeviceFlipDetector
}

View file

@ -299,6 +299,8 @@ private fun Blockchain.getSupportedTransactionExtras(): Network.TransactionExtra
Blockchain.Bitrock, Blockchain.BitrockTestnet,
Blockchain.Sonic, Blockchain.SonicTestnet,
Blockchain.ApeChain, Blockchain.ApeChainTestnet,
Blockchain.Scroll, Blockchain.ScrollTestnet,
Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet,
-> Network.TransactionExtrasType.NONE
// endregion
}

View file

@ -237,7 +237,7 @@ internal class DefaultStakingRepository(
): StakingAction {
return withContext(dispatchers.io) {
val response = when (params.actionCommonType) {
StakingActionCommonType.Enter -> stakeKitApi.createEnterAction(
is StakingActionCommonType.Enter -> stakeKitApi.createEnterAction(
createActionRequestBody(
userWalletId,
network,
@ -267,7 +267,7 @@ internal class DefaultStakingRepository(
): StakingGasEstimate {
return withContext(dispatchers.io) {
val gasEstimateDTO = when (params.actionCommonType) {
StakingActionCommonType.Enter -> stakeKitApi.estimateGasOnEnter(
is StakingActionCommonType.Enter -> stakeKitApi.estimateGasOnEnter(
createActionRequestBody(
userWalletId,
network,
@ -582,13 +582,14 @@ internal class DefaultStakingRepository(
-> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes())
Blockchain.BSC,
Blockchain.Ethereum,
Blockchain.TON,
Blockchain.Cardano,
-> TransactionData.Compiled.Data.RawString(unsignedTransaction)
Blockchain.Tron -> {
val tronStakeKitTransaction = tronStakeKitTransactionAdapter.fromJson(unsignedTransaction)
?: error("Failed to parse Tron StakeKit transaction")
TransactionData.Compiled.Data.RawString(tronStakeKitTransaction.rawDataHex)
}
Blockchain.TON -> TransactionData.Compiled.Data.RawString(unsignedTransaction)
else -> error("Unsupported blockchain")
}
}
@ -647,6 +648,7 @@ internal class DefaultStakingRepository(
const val KAVA_INTEGRATION_ID = "kava-kava-native-staking"
const val NEAR_INTEGRATION_ID = "near-near-native-staking"
const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking"
const val CARDANO_INTEGRATION_ID = "cardano-ada-native-staking"
const val ETHEREUM_POLYGON_APPROVE_SPENDER = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908"
@ -669,6 +671,7 @@ internal class DefaultStakingRepository(
// Blockchain.Kava.run { id + toCoinId() } to KAVA_INTEGRATION_ID,
// Blockchain.Near.run { id + toCoinId() } to NEAR_INTEGRATION_ID,
// Blockchain.Tezos.run { id + toCoinId() } to TEZOS_INTEGRATION_ID,
Blockchain.Cardano.run { id + toCoinId() } to CARDANO_INTEGRATION_ID,
)
}
}

View file

@ -29,7 +29,7 @@ dependencies {
implementation(projects.libs.blockchainSdk)
/** Project - Libs */
debugImplementation(projects.libs.visa)
implementation(projects.libs.visa)
/** Libs - Other */
implementation(deps.kotlin.coroutines)

View file

@ -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
}

View file

@ -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()

View file

@ -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")
}
}

View file

@ -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

View file

@ -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")

View file

@ -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)

View file

@ -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,22 +11,24 @@ 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()
@Volatile
private var config: VisaConfig? = null
@Volatile
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 {
if (provider != null) return@withLock requireNotNull(provider)
val config = getOrLoadConfig()
provider = VisaContractInfoProvider.Builder(
@ -54,33 +52,17 @@ internal class VisaLibLoader @Inject constructor(
}
}
private suspend fun createApi(): VisaApi = createMutex.withLock {
val config = getOrLoadConfig()
private suspend fun getOrLoadConfig(): VisaConfig = createMutex2.withLock {
if (config != null) return@withLock requireNotNull(config)
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 {
config = assetLoader.load<VisaConfig>(VISA_CONFIG_FILE_NAME)
return requireNotNull(config) {
"Visa config is not found"
return@withLock requireNotNull(config) {
"Visa config is not loaded"
}
}
companion object {
private const val VISA_CONFIG_FILE_NAME = "tangem-app-config/visa_config"
private const val X_ASN_HEADER_NAME = "x-asn"
}
}

View file

@ -1,6 +0,0 @@
package com.tangem.data.visa.di
import javax.inject.Qualifier
@Qualifier
internal annotation class ImplementedVisaRepository

View file

@ -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
}

View file

@ -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
}

View file

@ -0,0 +1,112 @@
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")
if (status is VisaCardActivationStatus.RefreshTokenExpired) {
throw RefreshTokenExpiredException()
}
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
}
}

View file

@ -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
}
}

View file

@ -32,7 +32,6 @@ internal class VisaCurrencyFactory {
available = balances.available.forPayment,
blocked = balances.blocked,
debt = balances.debt,
pendingRefund = balances.pendingRefund,
)
},
limits = VisaCurrency.Limits(

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -202,4 +202,24 @@ internal class DefaultWalletsRepository(
}
}
}
override fun nftEnabledStatus(userWalletId: UserWalletId): Flow<Boolean> {
return appPreferencesStore
.get(key = PreferencesKeys.WALLETS_WITH_NFT_ENABLED_KEY, default = emptySet())
.map { it.contains(userWalletId.stringValue) }
}
override suspend fun enableNFT(userWalletId: UserWalletId) {
appPreferencesStore.editData {
val added = it[PreferencesKeys.WALLETS_WITH_NFT_ENABLED_KEY].orEmpty()
it[PreferencesKeys.WALLETS_WITH_NFT_ENABLED_KEY] = added + userWalletId.stringValue
}
}
override suspend fun disableNFT(userWalletId: UserWalletId) {
appPreferencesStore.editData {
val added = it[PreferencesKeys.WALLETS_WITH_NFT_ENABLED_KEY].orEmpty()
it[PreferencesKeys.WALLETS_WITH_NFT_ENABLED_KEY] = added - userWalletId.stringValue
}
}
}