Updated on 2026-08-14
This commit is contained in:
commit
8148f32272
642 changed files with 10717 additions and 5789 deletions
|
|
@ -12,6 +12,7 @@ android {
|
|||
|
||||
dependencies {
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.androidx.appCompat)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ plugins {
|
|||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +47,6 @@ dependencies {
|
|||
/** Other */
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ plugins {
|
|||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ dependencies {
|
|||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
implementation(tangemDeps.blockchain)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
// endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ plugins {
|
|||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ dependencies {
|
|||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
implementation(deps.kotlin.serialization)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ plugins {
|
|||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ dependencies {
|
|||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
// endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ plugins {
|
|||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ dependencies {
|
|||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.firebase.crashlytics)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import com.tangem.datasource.local.token.StakingBalanceStore
|
|||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter
|
||||
import com.tangem.datasource.local.token.converter.TokenConverter
|
||||
import com.tangem.domain.common.TapWorkarounds.isWallet2
|
||||
import com.tangem.domain.staking.model.StakingApproval
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingEntryInfo
|
||||
|
|
@ -57,6 +58,7 @@ import com.tangem.domain.tokens.model.Network
|
|||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isCardano
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isSolana
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
|
@ -247,6 +249,7 @@ internal class DefaultStakingRepository(
|
|||
private fun checkFeatureToggleEnabled(networkId: Network.ID): Boolean {
|
||||
return when (Blockchain.fromId(networkId.value)) {
|
||||
Blockchain.TON -> stakingFeatureToggles.isTonStakingEnabled
|
||||
Blockchain.Cardano -> stakingFeatureToggles.isCardanoStakingEnabled
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
|
@ -255,14 +258,11 @@ internal class DefaultStakingRepository(
|
|||
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
|
||||
error("Failed to get user wallet")
|
||||
}
|
||||
|
||||
val blockchainId = cryptoCurrency.network.id.value
|
||||
return when {
|
||||
isSolana(cryptoCurrency.network.id.value) -> {
|
||||
INVALID_BATCHES_FOR_SOLANA.contains(userWallet.scanResponse.card.batchId)
|
||||
}
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
isSolana(blockchainId) -> INVALID_BATCHES_FOR_SOLANA.contains(userWallet.scanResponse.card.batchId)
|
||||
isCardano(blockchainId) -> !userWallet.scanResponse.card.isWallet2
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -273,7 +273,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,
|
||||
|
|
@ -303,7 +303,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,
|
||||
|
|
@ -674,13 +674,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")
|
||||
}
|
||||
}
|
||||
|
|
@ -749,6 +750,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"
|
||||
|
||||
|
|
@ -771,6 +773,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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,4 +9,7 @@ internal class DefaultStakingFeatureToggles(
|
|||
|
||||
override val isTonStakingEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_TON_ENABLED")
|
||||
|
||||
override val isCardanoStakingEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("STAKING_CARDANO_ENABLED")
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ plugins {
|
|||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +51,6 @@ dependencies {
|
|||
implementation(deps.timber)
|
||||
implementation(deps.retrofit) // For HttpException
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ dependencies {
|
|||
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.pagination)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ package com.tangem.data.txhistory.di
|
|||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.txhistory.repository.DefaultTxHistoryRepository
|
||||
import com.tangem.data.txhistory.repository.RefactoredTxHistoryRepository
|
||||
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepository
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -32,4 +34,18 @@ internal object TxHistoryDataModule {
|
|||
txHistoryItemsStore,
|
||||
dispatchers,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTxHistoryRepositoryV2(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
txHistoryItemsStore: TxHistoryItemsStore,
|
||||
cacheRegistry: CacheRegistry,
|
||||
): TxHistoryRepositoryV2 = RefactoredTxHistoryRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = dispatchers,
|
||||
txHistoryItemsStore = txHistoryItemsStore,
|
||||
cacheRegistry = cacheRegistry,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package com.tangem.data.txhistory.repository
|
||||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.txhistory.repository.paging.TxHistoryPageBatchFetcher
|
||||
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
|
||||
import com.tangem.domain.txhistory.model.TxHistoryListBatchFlow
|
||||
import com.tangem.domain.txhistory.model.TxHistoryListBatchingContext
|
||||
import com.tangem.domain.txhistory.model.TxHistoryListConfig
|
||||
import com.tangem.domain.txhistory.models.Page
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.walletmanager.utils.SdkPageConverter
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.BatchListSource
|
||||
import com.tangem.pagination.toBatchFlow
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import timber.log.Timber
|
||||
|
||||
internal class RefactoredTxHistoryRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val txHistoryItemsStore: TxHistoryItemsStore,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : TxHistoryRepositoryV2 {
|
||||
|
||||
private val sdkPageConverter = SdkPageConverter()
|
||||
private val TxHistoryListConfig.storeKey get() = TxHistoryItemsStore.Key(userWalletId, currency)
|
||||
|
||||
override fun getTxHistoryBatchFlow(batchSize: Int, context: TxHistoryListBatchingContext): TxHistoryListBatchFlow {
|
||||
return BatchListSource(
|
||||
fetchDispatcher = dispatchers.io,
|
||||
context = context,
|
||||
generateNewKey = { keys -> keys.lastOrNull()?.inc() ?: 0 },
|
||||
batchFetcher = createFetcher(batchSize),
|
||||
).toBatchFlow()
|
||||
}
|
||||
|
||||
private fun createFetcher(
|
||||
batchSize: Int,
|
||||
): TxHistoryPageBatchFetcher<TxHistoryListConfig, PaginationWrapper<TxHistoryItem>> =
|
||||
TxHistoryPageBatchFetcher { request, _ ->
|
||||
val wrappedItems = loadItems(request, batchSize)
|
||||
BatchFetchResult.Success(
|
||||
data = wrappedItems,
|
||||
empty = wrappedItems.items.isEmpty(),
|
||||
last = wrappedItems.nextPage is Page.LastPage,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun loadItems(
|
||||
request: TxHistoryPageBatchFetcher.Request<TxHistoryListConfig>,
|
||||
batchSize: Int,
|
||||
): PaginationWrapper<TxHistoryItem> {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getTxHistoryPageKey(request.page, request.params),
|
||||
skipCache = request.params.refresh,
|
||||
block = { fetch(request, batchSize) },
|
||||
)
|
||||
|
||||
return txHistoryItemsStore.getSync(request.page, request.params)
|
||||
}
|
||||
|
||||
private suspend fun fetch(request: TxHistoryPageBatchFetcher.Request<TxHistoryListConfig>, batchSize: Int) {
|
||||
val wrappedItems = walletManagersFacade.getTxHistoryItems(
|
||||
userWalletId = request.params.userWalletId,
|
||||
currency = request.params.currency,
|
||||
page = sdkPageConverter.convertBack(request.page),
|
||||
pageSize = batchSize,
|
||||
)
|
||||
|
||||
txHistoryItemsStore.store(key = request.params.storeKey, value = wrappedItems)
|
||||
}
|
||||
|
||||
private suspend fun TxHistoryItemsStore.getSync(
|
||||
pageToLoad: Page,
|
||||
config: TxHistoryListConfig,
|
||||
): PaginationWrapper<TxHistoryItem> {
|
||||
val storedItems = requireNotNull(getSyncOrNull(config.storeKey, pageToLoad)) {
|
||||
"The transaction history page #$pageToLoad could not be retrieved"
|
||||
}
|
||||
|
||||
return if (pageToLoad is Page.Initial) storedItems.addRecentTransactions(config) else storedItems
|
||||
}
|
||||
|
||||
private suspend fun PaginationWrapper<TxHistoryItem>.addRecentTransactions(
|
||||
config: TxHistoryListConfig,
|
||||
): PaginationWrapper<TxHistoryItem> {
|
||||
val recentItems = walletManagersFacade.getRecentTransactions(
|
||||
userWalletId = config.userWalletId,
|
||||
currency = config.currency,
|
||||
)
|
||||
.filterUnconfirmedTransaction()
|
||||
.sortedByDescending { it.timestampInMillis }
|
||||
.filterIfTxAlreadyAdded(apiItems = items)
|
||||
|
||||
return if (recentItems.isEmpty()) {
|
||||
Timber.d("Nothing to add to TxHistory")
|
||||
this
|
||||
} else {
|
||||
Timber.d(
|
||||
"Recent transactions were added to TxHistory: %s",
|
||||
recentItems.joinToString(
|
||||
prefix = "[",
|
||||
postfix = "]",
|
||||
transform = TxHistoryItem::txHash,
|
||||
),
|
||||
)
|
||||
|
||||
return copy(items = recentItems + items)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<TxHistoryItem>.filterUnconfirmedTransaction(): List<TxHistoryItem> {
|
||||
return filter { it.status == TxHistoryItem.TransactionStatus.Unconfirmed }
|
||||
}
|
||||
|
||||
private fun List<TxHistoryItem>.filterIfTxAlreadyAdded(apiItems: List<TxHistoryItem>): List<TxHistoryItem> {
|
||||
return filter { item -> apiItems.none { it.txHash == item.txHash } }
|
||||
}
|
||||
|
||||
private fun getTxHistoryPageKey(page: Page, config: TxHistoryListConfig): String {
|
||||
return "tx_history_page_${config.currency}_${config.userWalletId}_$page"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.data.txhistory.repository.paging
|
||||
|
||||
import com.tangem.domain.txhistory.models.Page
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.exception.EndOfPaginationException
|
||||
import com.tangem.pagination.fetcher.BatchFetcher
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class TxHistoryPageBatchFetcher<TRequestParams : Any, TData : PaginationWrapper<TxHistoryItem>>(
|
||||
private val subFetcher: SubFetcher<TRequestParams, TData>,
|
||||
) : BatchFetcher<TRequestParams, TData> {
|
||||
data class Request<TRequestParams>(val page: Page, val params: TRequestParams)
|
||||
fun interface SubFetcher<TRequestParams : Any, TData> {
|
||||
suspend fun fetch(
|
||||
request: Request<TRequestParams>,
|
||||
lastResult: BatchFetchResult<TData>?,
|
||||
): BatchFetchResult<TData>
|
||||
}
|
||||
|
||||
private val lastRequest = MutableStateFlow<Request<TRequestParams>?>(null)
|
||||
|
||||
override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult<TData> {
|
||||
val req = Request(
|
||||
page = Page.Initial,
|
||||
params = requestParams,
|
||||
)
|
||||
|
||||
val res = runCatching {
|
||||
subFetcher.fetch(request = req, lastResult = null)
|
||||
}.getOrElse {
|
||||
currentCoroutineContext().ensureActive()
|
||||
BatchFetchResult.Error(it)
|
||||
}
|
||||
|
||||
lastRequest.value = req
|
||||
return res
|
||||
}
|
||||
|
||||
override suspend fun fetchNext(
|
||||
overrideRequestParams: TRequestParams?,
|
||||
lastResult: BatchFetchResult<TData>,
|
||||
): BatchFetchResult<TData> {
|
||||
val last = lastRequest.value
|
||||
requireNotNull(last)
|
||||
|
||||
val req = if (lastResult is BatchFetchResult.Success) {
|
||||
if (lastResult.last && overrideRequestParams == null) {
|
||||
return BatchFetchResult.Error(EndOfPaginationException())
|
||||
}
|
||||
|
||||
Request(
|
||||
page = lastResult.data.nextPage,
|
||||
params = overrideRequestParams ?: last.params,
|
||||
)
|
||||
} else {
|
||||
last
|
||||
}
|
||||
|
||||
val res = runCatching {
|
||||
subFetcher.fetch(request = req, lastResult = lastResult)
|
||||
}.getOrElse {
|
||||
currentCoroutineContext().ensureActive()
|
||||
BatchFetchResult.Error(it)
|
||||
}
|
||||
|
||||
lastRequest.value = req
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ plugins {
|
|||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +30,7 @@ dependencies {
|
|||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
/** Project - Libs */
|
||||
debugImplementation(projects.libs.visa)
|
||||
implementation(projects.libs.visa)
|
||||
|
||||
/** Libs - Other */
|
||||
implementation(deps.kotlin.coroutines)
|
||||
|
|
@ -39,7 +40,7 @@ dependencies {
|
|||
implementation(deps.timber)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.moshi.kotlin)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
|
||||
/** Libs - Tangem */
|
||||
|
|
|
|||
|
|
@ -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,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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,6 @@ internal class VisaCurrencyFactory {
|
|||
available = balances.available.forPayment,
|
||||
blocked = balances.blocked,
|
||||
debt = balances.debt,
|
||||
pendingRefund = balances.pendingRefund,
|
||||
)
|
||||
},
|
||||
limits = VisaCurrency.Limits(
|
||||
|
|
@ -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
|
||||
|
|
@ -11,6 +11,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
|
|||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SEED_FIRST_NOTIFICATION_SHOW_TIME
|
||||
import com.tangem.datasource.local.preferences.utils.get
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
|
|
@ -202,4 +203,28 @@ internal class DefaultWalletsRepository(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun nftEnabledStatus(userWalletId: UserWalletId): Flow<Boolean> = appPreferencesStore
|
||||
.getObjectMap<Boolean>(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY)
|
||||
.map { it[userWalletId.stringValue] == true }
|
||||
|
||||
override suspend fun enableNFT(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.editData {
|
||||
it.setObjectMap(
|
||||
key = PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY,
|
||||
value = it.getObjectMap<Boolean>(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY)
|
||||
.plus(userWalletId.stringValue to true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun disableNFT(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.editData {
|
||||
it.setObjectMap(
|
||||
key = PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY,
|
||||
value = it.getObjectMap<Boolean>(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY)
|
||||
.plus(userWalletId.stringValue to false),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue