Updated on 2026-08-14
This commit is contained in:
commit
bf0cb06cb3
297 changed files with 4771 additions and 2376 deletions
|
|
@ -294,6 +294,8 @@ private fun Blockchain.getSupportedTransactionExtras(): Network.TransactionExtra
|
|||
Blockchain.VanarChainTestnet,
|
||||
Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet,
|
||||
Blockchain.Bitrock, Blockchain.BitrockTestnet,
|
||||
Blockchain.Sonic, Blockchain.SonicTestnet,
|
||||
Blockchain.ApeChain, Blockchain.ApeChainTestnet,
|
||||
-> Network.TransactionExtrasType.NONE
|
||||
// endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
|
|
@ -44,4 +46,6 @@ dependencies {
|
|||
/** Other */
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
|
|
@ -39,5 +41,7 @@ dependencies {
|
|||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.tangem.blockchain)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
// endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
|
|
@ -24,6 +26,7 @@ dependencies {
|
|||
implementation(projects.domain.onramp)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.appTheme.models)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
// region DI
|
||||
|
||||
|
|
@ -41,6 +44,10 @@ dependencies {
|
|||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
implementation(deps.kotlin.serialization)
|
||||
|
||||
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.libs.crypto)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.data.onramp
|
||||
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.onramp.converters.HotCryptoCurrencyConverter
|
||||
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.onramp.model.HotCryptoCurrency
|
||||
import com.tangem.domain.onramp.repositories.HotCryptoRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* Default implementation of [HotCryptoRepository]
|
||||
*
|
||||
* @property excludedBlockchains excluded blockchains
|
||||
* @property hotCryptoResponseStore store of `HotCryptoResponse`
|
||||
* @property userWalletsStore store of `UserWallet`
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultHotCryptoRepository(
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val hotCryptoResponseStore: HotCryptoResponseStore,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
) : HotCryptoRepository {
|
||||
|
||||
override fun getCurrencies(userWalletId: UserWalletId): Flow<List<HotCryptoCurrency>> {
|
||||
return hotCryptoResponseStore.get()
|
||||
.map {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(userWalletId)
|
||||
?: error("UserWalletId [$userWalletId] not found")
|
||||
|
||||
HotCryptoCurrencyConverter(
|
||||
scanResponse = userWallet.scanResponse,
|
||||
imageHost = it.imageHost,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
).convertList(input = it.tokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.tangem.data.onramp.converters
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.getNetwork
|
||||
import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.onramp.model.HotCryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from [HotCryptoResponse.Token] to [HotCryptoCurrency]
|
||||
*
|
||||
* @property scanResponse scan response
|
||||
* @property imageHost image host
|
||||
* @property excludedBlockchains excluded blockchains
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class HotCryptoCurrencyConverter(
|
||||
private val scanResponse: ScanResponse,
|
||||
private val imageHost: String?,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
) : Converter<HotCryptoResponse.Token, HotCryptoCurrency> {
|
||||
|
||||
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) }
|
||||
|
||||
override fun convert(value: HotCryptoResponse.Token): HotCryptoCurrency {
|
||||
val network = createNetwork(value.networkId)
|
||||
|
||||
val contractAddress = value.contractAddress
|
||||
val decimals = value.decimalCount
|
||||
val currency = if (contractAddress != null && decimals != null) {
|
||||
cryptoCurrencyFactory.createToken(
|
||||
network = network,
|
||||
rawId = value.id,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
decimals = decimals,
|
||||
contractAddress = contractAddress,
|
||||
)
|
||||
} else {
|
||||
cryptoCurrencyFactory.createCoin(network = network)
|
||||
}
|
||||
|
||||
return HotCryptoCurrency(
|
||||
cryptoCurrency = currency.setupIconUrl(id = value.id),
|
||||
fiatRate = value.currentPrice,
|
||||
priceChange = value.priceChangePercentage,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNetwork(networkId: String): Network {
|
||||
return requireNotNull(
|
||||
getNetwork(
|
||||
blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)),
|
||||
extraDerivationPath = null,
|
||||
scanResponse = scanResponse,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.setupIconUrl(id: String): CryptoCurrency {
|
||||
imageHost ?: return this
|
||||
|
||||
val iconUrl = "${imageHost}large/$id.png"
|
||||
|
||||
return when (this) {
|
||||
is CryptoCurrency.Coin -> copy(iconUrl = iconUrl)
|
||||
is CryptoCurrency.Token -> copy(iconUrl = iconUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.data.onramp.di
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.onramp.DefaultHotCryptoRepository
|
||||
import com.tangem.data.onramp.DefaultOnrampErrorResolver
|
||||
import com.tangem.data.onramp.DefaultOnrampRepository
|
||||
import com.tangem.data.onramp.DefaultOnrampTransactionRepository
|
||||
|
|
@ -10,12 +12,15 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
|
|||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.crypto.DataSignatureVerifier
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
|
||||
import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore
|
||||
import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore
|
||||
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
|
||||
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
|
||||
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.onramp.repositories.HotCryptoRepository
|
||||
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
||||
import com.tangem.domain.onramp.repositories.OnrampRepository
|
||||
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
|
||||
|
|
@ -83,4 +88,18 @@ internal object OnrampDataModule {
|
|||
OnrampErrorConverter(jsonAdapter),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHotCryptoRepository(
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
hotCryptoResponseStore: HotCryptoResponseStore,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
): HotCryptoRepository {
|
||||
return DefaultHotCryptoRepository(
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
hotCryptoResponseStore = hotCryptoResponseStore,
|
||||
userWalletsStore = userWalletsStore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +1,32 @@
|
|||
package com.tangem.data.promo
|
||||
|
||||
import com.tangem.data.promo.converters.PromoResponseConverter
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SWAP_STORIES_KEY
|
||||
import com.tangem.datasource.local.preferences.utils.get
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.domain.promo.models.PromoBanner
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
internal class DefaultPromoRepository(
|
||||
private val tangemApi: TangemTechApi,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : PromoRepository {
|
||||
|
||||
private val promoResponseConverter = PromoResponseConverter()
|
||||
override suspend fun getChangellyPromoBanner(): PromoBanner? {
|
||||
return runCatching(dispatchers.io) {
|
||||
promoResponseConverter.convert(
|
||||
tangemApi.getPromotionInfo(CHANGELLY_NAME)
|
||||
.getOrThrow(),
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getOkxPromoBanner(): PromoBanner? {
|
||||
return runCatching(dispatchers.io) {
|
||||
promoResponseConverter.convert(
|
||||
tangemApi.getPromotionInfo(OKX)
|
||||
.getOrThrow(),
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getRingPromoBanner(): PromoBanner? {
|
||||
return runCatching(dispatchers.io) {
|
||||
promoResponseConverter.convert(
|
||||
tangemApi.getPromotionInfo(RING)
|
||||
.getOrThrow(),
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override fun isReadyToShowWalletSwapPromo(): Flow<Boolean> {
|
||||
return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY, true)
|
||||
return flowOf(false) // Use it on new promo action
|
||||
}
|
||||
|
||||
override fun isReadyToShowTokenSwapPromo(): Flow<Boolean> {
|
||||
return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY, true)
|
||||
return flowOf(false) // Use it on new promo action
|
||||
}
|
||||
|
||||
override suspend fun setNeverToShowWalletSwapPromo() {
|
||||
appPreferencesStore.store(
|
||||
key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY,
|
||||
value = false,
|
||||
)
|
||||
// Use it on new promo action
|
||||
}
|
||||
|
||||
override suspend fun setNeverToShowTokenSwapPromo() {
|
||||
appPreferencesStore.store(
|
||||
key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY,
|
||||
value = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun isReadyToShowRingPromo(userWalletId: UserWalletId): Flow<Boolean> {
|
||||
return combine(
|
||||
flow = appPreferencesStore
|
||||
.get(key = ADDED_WALLETS_WITH_RING_KEY, default = emptySet())
|
||||
.map { userWalletId.stringValue in it },
|
||||
flow2 = appPreferencesStore.get(key = SHOULD_SHOW_RING_PROMO_KEY, default = true),
|
||||
transform = { isRingAdded, shouldShowRingPromo -> isRingAdded && shouldShowRingPromo },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setNeverToShowRingPromo() {
|
||||
appPreferencesStore.store(key = SHOULD_SHOW_RING_PROMO_KEY, value = false)
|
||||
// Use it on new promo action
|
||||
}
|
||||
|
||||
override fun isReadyToShowSwapStories(): Flow<Boolean> {
|
||||
|
|
@ -105,10 +43,4 @@ internal class DefaultPromoRepository(
|
|||
value = false,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val CHANGELLY_NAME = "changelly"
|
||||
private const val OKX = "okx"
|
||||
private const val RING = "ring"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
package com.tangem.data.promo.di
|
||||
|
||||
import com.tangem.data.promo.DefaultPromoRepository
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -17,11 +15,7 @@ internal object PromoDataModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePromoRepository(
|
||||
tangemTechApi: TangemTechApi,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): PromoRepository {
|
||||
return DefaultPromoRepository(tangemTechApi, appPreferencesStore, dispatchers)
|
||||
fun providePromoRepository(appPreferencesStore: AppPreferencesStore): PromoRepository {
|
||||
return DefaultPromoRepository(appPreferencesStore)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
|
|
@ -31,5 +33,7 @@ dependencies {
|
|||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
// endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
|
|
@ -45,6 +47,8 @@ dependencies {
|
|||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.libs.crypto)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
|
|
@ -48,4 +50,6 @@ dependencies {
|
|||
implementation(deps.timber)
|
||||
implementation(deps.retrofit) // For HttpException
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
|||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.tokens.repository.*
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
||||
import com.tangem.datasource.local.network.NetworksStatusesStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -32,6 +33,7 @@ internal object TokensDataModule {
|
|||
cacheRegistry: CacheRegistry,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
expressServiceLoader: ExpressServiceLoader,
|
||||
hotCryptoLoader: HotCryptoLoader,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
): CurrenciesRepository {
|
||||
return DefaultCurrenciesRepository(
|
||||
|
|
@ -42,6 +44,7 @@ internal object TokensDataModule {
|
|||
appPreferencesStore = appPreferencesStore,
|
||||
dispatchers = dispatchers,
|
||||
expressServiceLoader = expressServiceLoader,
|
||||
hotCryptoLoader = hotCryptoLoader,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRA
|
|||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
|
|
@ -33,10 +34,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
|
|||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency
|
||||
|
||||
|
|
@ -48,6 +47,7 @@ internal class DefaultCurrenciesRepository(
|
|||
private val cacheRegistry: CacheRegistry,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val expressServiceLoader: ExpressServiceLoader,
|
||||
private val hotCryptoLoader: HotCryptoLoader,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
) : CurrenciesRepository {
|
||||
|
|
@ -601,7 +601,11 @@ internal class DefaultCurrenciesRepository(
|
|||
network = token.networkId,
|
||||
)
|
||||
}
|
||||
expressServiceLoader.update(userWalletId, tokens)
|
||||
|
||||
coroutineScope {
|
||||
launch { expressServiceLoader.update(userWalletId, tokens) }
|
||||
launch { hotCryptoLoader.fetch(tokens = userTokens.tokens) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchExpressAssetsByNetworkIds(
|
||||
|
|
@ -619,7 +623,12 @@ internal class DefaultCurrenciesRepository(
|
|||
cacheRegistry.invokeOnExpire(
|
||||
key = getAssetsCacheKey(userWalletId),
|
||||
skipCache = refresh,
|
||||
block = { expressServiceLoader.update(userWalletId, tokens) },
|
||||
block = {
|
||||
coroutineScope {
|
||||
launch { expressServiceLoader.update(userWalletId, tokens) }
|
||||
launch { hotCryptoLoader.update(cryptoCurrencies) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
|
|
@ -37,6 +39,8 @@ dependencies {
|
|||
implementation(deps.timber)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.moshi.kotlin)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
|
||||
/** Libs - Tangem */
|
||||
implementation(deps.tangem.blockchain)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.data.visa.config
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class VisaConfig(
|
||||
@Json(name = "testnet")
|
||||
val testnet: Addresses,
|
||||
|
|
@ -11,6 +13,7 @@ internal data class VisaConfig(
|
|||
val header: Header,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Addresses(
|
||||
@Json(name = "paymentAccountRegistry")
|
||||
val paymentAccountRegistry: String,
|
||||
|
|
@ -18,6 +21,7 @@ internal data class VisaConfig(
|
|||
val bridgeProcessor: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Header(
|
||||
@Json(name = "x-asn")
|
||||
val xAsn: String,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,28 +1,49 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.data.visa.converter.VisaActivationStatusConverter
|
||||
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
|
||||
import com.tangem.datasource.api.visa.TangemVisaApi
|
||||
import com.tangem.domain.visa.model.ActivationOrder
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class DefaultVisaActivationRepository @Inject constructor(
|
||||
@Suppress("UnusedPrivateMember")
|
||||
internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
||||
@Assisted private val cardId: String,
|
||||
private val visaApi: TangemVisaApi,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
private val visaActivationStatusConverter: VisaActivationStatusConverter,
|
||||
private val visaAuthProvider: TangemVisaAuthProvider,
|
||||
) : VisaActivationRepository {
|
||||
|
||||
override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) {
|
||||
visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus())
|
||||
// visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus(visaAuthProvider.getAuthHeader(cardId)))
|
||||
VisaActivationRemoteState.CardWalletSignatureRequired // mock
|
||||
// TODO implement refreshing access token if it's expired
|
||||
}
|
||||
|
||||
override suspend fun getActivationOrderToSign(): ActivationOrder {
|
||||
return ActivationOrder("TODO implement")
|
||||
override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState =
|
||||
withContext(dispatcherProvider.io) {
|
||||
// visaActivationStatusConverter.convert(
|
||||
// visaApi.getRemoteActivationStatusLongPoll(visaAuthProvider.getAuthHeader(cardId)),
|
||||
// )
|
||||
|
||||
VisaActivationRemoteState.WaitingPinCode
|
||||
}
|
||||
|
||||
override suspend fun getActivationOrderToSign(): ActivationOrder = withContext(dispatcherProvider.io) {
|
||||
ActivationOrder(CryptoUtils.generateRandomBytes(length = 32).toHexString())
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : VisaActivationRepository.Factory {
|
||||
override fun create(cardId: String): DefaultVisaActivationRepository
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.datasource.api.visa.TangemVisaAuthApi
|
||||
import com.tangem.domain.visa.model.VisaAuthChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthSession
|
||||
|
|
@ -10,6 +12,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
internal class DefaultVisaAuthRepository @Inject constructor(
|
||||
private val visaAuthApi: TangemVisaAuthApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -17,14 +20,19 @@ internal class DefaultVisaAuthRepository @Inject constructor(
|
|||
|
||||
override suspend fun getCardAuthChallenge(cardId: String, cardPublicKey: String): VisaAuthChallenge.Card =
|
||||
withContext(dispatchers.io) {
|
||||
val response = visaAuthApi.generateNonceByCard(
|
||||
cardId = cardId,
|
||||
cardPublicKey = cardPublicKey,
|
||||
)
|
||||
// val response = visaAuthApi.generateNonceByCard(
|
||||
// cardId = cardId,
|
||||
// cardPublicKey = cardPublicKey,
|
||||
// )
|
||||
//
|
||||
// VisaAuthChallenge.Card(
|
||||
// challenge = response.nonce,
|
||||
// session = VisaAuthSession(response.sessionId),
|
||||
// )
|
||||
|
||||
VisaAuthChallenge.Card(
|
||||
challenge = response.nonce,
|
||||
session = VisaAuthSession(response.sessionId),
|
||||
challenge = CryptoUtils.generateRandomBytes(length = 16).toHexString(),
|
||||
session = VisaAuthSession("session"),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -32,39 +40,56 @@ internal class DefaultVisaAuthRepository @Inject constructor(
|
|||
cardId: String,
|
||||
walletPublicKey: String,
|
||||
): VisaAuthChallenge.Wallet = withContext(dispatchers.io) {
|
||||
val response = visaAuthApi.generateNonceByWalletAddress(
|
||||
customerId = cardId,
|
||||
customerWalletAddress = walletPublicKey,
|
||||
)
|
||||
|
||||
// val response = visaAuthApi.generateNonceByWalletAddress(
|
||||
// customerId = cardId,
|
||||
// customerWalletAddress = walletPublicKey,
|
||||
// )
|
||||
//
|
||||
// VisaAuthChallenge.Wallet(
|
||||
// challenge = response.nonce,
|
||||
// session = VisaAuthSession(response.sessionId),
|
||||
// )
|
||||
VisaAuthChallenge.Wallet(
|
||||
challenge = response.nonce,
|
||||
session = VisaAuthSession(response.sessionId),
|
||||
challenge = CryptoUtils.generateRandomBytes(length = 32).toHexString(),
|
||||
session = VisaAuthSession("session"),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens =
|
||||
withContext(dispatchers.io) {
|
||||
val response = when (signedChallenge) {
|
||||
is VisaAuthSignedChallenge.ByCardPublicKey -> {
|
||||
visaAuthApi.getAccessToken(
|
||||
sessionId = signedChallenge.challenge.session.sessionId,
|
||||
signature = signedChallenge.signature,
|
||||
salt = signedChallenge.salt,
|
||||
)
|
||||
}
|
||||
is VisaAuthSignedChallenge.ByWallet -> {
|
||||
visaAuthApi.getAccessToken(
|
||||
sessionId = signedChallenge.challenge.session.sessionId,
|
||||
signature = signedChallenge.signature,
|
||||
salt = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// val response = when (signedChallenge) {
|
||||
// is VisaAuthSignedChallenge.ByCardPublicKey -> {
|
||||
// visaAuthApi.getAccessToken(
|
||||
// sessionId = signedChallenge.challenge.session.sessionId,
|
||||
// signature = signedChallenge.signature,
|
||||
// salt = signedChallenge.salt,
|
||||
// )
|
||||
// }
|
||||
// is VisaAuthSignedChallenge.ByWallet -> {
|
||||
// visaAuthApi.getAccessToken(
|
||||
// sessionId = signedChallenge.challenge.session.sessionId,
|
||||
// signature = signedChallenge.signature,
|
||||
// salt = null,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// VisaAuthTokens(
|
||||
// accessToken = response.accessToken,
|
||||
// refreshToken = response.refreshToken,
|
||||
// )
|
||||
VisaAuthTokens(
|
||||
accessToken = response.accessToken,
|
||||
refreshToken = response.refreshToken,
|
||||
accessToken = "accessToken",
|
||||
refreshToken = VisaAuthTokens.RefreshToken("refreshToken"),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens =
|
||||
withContext(dispatchers.io) {
|
||||
// TODO
|
||||
VisaAuthTokens(
|
||||
accessToken = "accessToken",
|
||||
refreshToken = VisaAuthTokens.RefreshToken("new refreshToken"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,5 +38,7 @@ internal interface VisaDataBindsModule {
|
|||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindVisaActivationRepository(repository: DefaultVisaActivationRepository): VisaActivationRepository
|
||||
fun bindVisaActivationRepositoryFactory(
|
||||
repository: DefaultVisaActivationRepository.Factory,
|
||||
): VisaActivationRepository.Factory
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue