Updated on 2026-08-14
This commit is contained in:
parent
a0ba5ab9d9
commit
286e28223c
7 changed files with 248 additions and 14 deletions
|
|
@ -38,9 +38,6 @@ dependencies {
|
|||
implementation(projects.domain.common)
|
||||
implementation(projects.features.swap.domain)
|
||||
|
||||
/** Feature API - remove after removing [HotWalletFeatureToggles] */
|
||||
implementation(projects.features.hotWallet.api)
|
||||
|
||||
|
||||
/** Project - Utils */
|
||||
implementation(projects.core.utils)
|
||||
|
|
@ -51,6 +48,7 @@ dependencies {
|
|||
implementation(projects.libs.visa)
|
||||
|
||||
/** Libs - Other */
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.arrow.fx)
|
||||
|
|
@ -68,6 +66,6 @@ dependencies {
|
|||
implementation(projects.libs.tangemSdkApi)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.core)
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.data.pay.converter
|
||||
|
||||
import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convert
|
||||
import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convertBack
|
||||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.pay.PaymentAccountStatus
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
|
||||
/**
|
||||
* Two-way converter between [PaymentAccountStatus] and [PaymentAccountStatusDM].
|
||||
*
|
||||
* [convert] maps domain → data model. Returns null for transient statuses that should not be persisted
|
||||
* (Loading, ExposedDevice, Unavailable, NotSynced).
|
||||
*
|
||||
* [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source.
|
||||
*/
|
||||
internal object PaymentAccountStatusDMConverter :
|
||||
TwoWayConverter<PaymentAccountStatus, PaymentAccountStatusDM?> {
|
||||
|
||||
override fun convert(value: PaymentAccountStatus): PaymentAccountStatusDM? {
|
||||
return when (value) {
|
||||
is PaymentAccountStatus.NotCreated -> PaymentAccountStatusDM.NotCreated()
|
||||
is PaymentAccountStatus.UnderReview -> PaymentAccountStatusDM.UnderReview(kycStatus = value.kycStatus)
|
||||
is PaymentAccountStatus.IssuingCard -> PaymentAccountStatusDM.IssuingCard()
|
||||
is PaymentAccountStatus.Locked -> PaymentAccountStatusDM.Locked()
|
||||
is PaymentAccountStatus.Loaded -> PaymentAccountStatusDM.Loaded(
|
||||
cardId = value.cardId,
|
||||
lastFourDigits = value.lastFourDigits,
|
||||
balance = value.balance,
|
||||
currencyCode = value.currencyCode,
|
||||
depositAddress = value.depositAddress,
|
||||
isPinSet = value.isPinSet,
|
||||
)
|
||||
is PaymentAccountStatus.Error.CardIssueFailed -> PaymentAccountStatusDM.CardIssueFailed()
|
||||
// Transient statuses are not persisted
|
||||
is PaymentAccountStatus.Loading,
|
||||
is PaymentAccountStatus.Error.ExposedDevice,
|
||||
is PaymentAccountStatus.Error.Unavailable,
|
||||
is PaymentAccountStatus.Error.NotSynced,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertBack(value: PaymentAccountStatusDM?): PaymentAccountStatus {
|
||||
return when (value) {
|
||||
is PaymentAccountStatusDM.CardIssueFailed -> PaymentAccountStatus.Error.CardIssueFailed
|
||||
is PaymentAccountStatusDM.NotCreated -> PaymentAccountStatus.NotCreated
|
||||
is PaymentAccountStatusDM.IssuingCard -> PaymentAccountStatus.IssuingCard(source = StatusSource.CACHE)
|
||||
is PaymentAccountStatusDM.Locked -> PaymentAccountStatus.Locked(source = StatusSource.CACHE)
|
||||
is PaymentAccountStatusDM.UnderReview -> PaymentAccountStatus.UnderReview(
|
||||
source = StatusSource.CACHE,
|
||||
kycStatus = value.kycStatus,
|
||||
)
|
||||
is PaymentAccountStatusDM.Loaded -> PaymentAccountStatus.Loaded(
|
||||
source = StatusSource.CACHE,
|
||||
cardId = value.cardId,
|
||||
lastFourDigits = value.lastFourDigits,
|
||||
balance = value.balance,
|
||||
currencyCode = value.currencyCode,
|
||||
depositAddress = value.depositAddress,
|
||||
isPinSet = value.isPinSet,
|
||||
)
|
||||
null -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.CACHE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,23 @@
|
|||
package com.tangem.data.pay.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
|
||||
import com.tangem.data.pay.DefaultTangemPayEligibilityManager
|
||||
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher
|
||||
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer
|
||||
import com.tangem.data.pay.repository.*
|
||||
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
||||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase
|
||||
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
|
|
@ -21,11 +31,15 @@ import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
|||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -92,6 +106,28 @@ internal interface TangemPayDataModule {
|
|||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePaymentAccountStatusesStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): PaymentAccountStatusesStore {
|
||||
return PaymentAccountStatusesStore(
|
||||
runtimeStore = RuntimeSharedStore(),
|
||||
persistenceDataStore = DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = mapWithStringKeyTypes<PaymentAccountStatusDM>(),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
),
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePaymentAccountStatusSupplier(
|
||||
|
|
|
|||
|
|
@ -1,24 +1,86 @@
|
|||
package com.tangem.data.pay.store
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.PaymentAccountStatus
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("UnusedParameter", "EmptyFunctionBlock", "FunctionOnlyReturningConstant")
|
||||
@Singleton
|
||||
internal class PaymentAccountStatusesStore @Inject constructor() {
|
||||
internal typealias WalletIdWithPaymentStatus = Map<String, PaymentAccountStatus>
|
||||
internal typealias WalletIdWithPaymentStatusDM = Map<String, PaymentAccountStatusDM>
|
||||
|
||||
/**
|
||||
* Store for payment account statuses with dual storage (runtime + persistence).
|
||||
*
|
||||
* @property runtimeStore runtime store for fast in-memory access
|
||||
* @property persistenceDataStore persistence store for caching across app restarts
|
||||
*/
|
||||
internal class PaymentAccountStatusesStore(
|
||||
private val runtimeStore: RuntimeSharedStore<WalletIdWithPaymentStatus>,
|
||||
private val persistenceDataStore: DataStore<WalletIdWithPaymentStatusDM>,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io)
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
try {
|
||||
val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch
|
||||
runtimeStore.store(
|
||||
value = cachedStatuses.mapValues { (_, statusDM) ->
|
||||
PaymentAccountStatusDMConverter.convertBack(statusDM)
|
||||
},
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error while loading cached payment account statuses")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun get(userWalletId: UserWalletId): Flow<PaymentAccountStatus> {
|
||||
return emptyFlow()
|
||||
return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] }
|
||||
}
|
||||
|
||||
fun getSyncOrNull(userWalletId: UserWalletId): PaymentAccountStatus? {
|
||||
return null
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId): PaymentAccountStatus? {
|
||||
return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue)
|
||||
}
|
||||
|
||||
fun store(userWalletId: UserWalletId, status: PaymentAccountStatus) {
|
||||
suspend fun store(userWalletId: UserWalletId, status: PaymentAccountStatus) {
|
||||
coroutineScope {
|
||||
launch { storeInRuntime(userWalletId = userWalletId, status = status) }
|
||||
launch { storeInPersistence(userWalletId = userWalletId, status = status) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun contains(userWalletId: UserWalletId): Boolean {
|
||||
return runtimeStore.getSyncOrDefault(emptyMap()).containsKey(userWalletId.stringValue)
|
||||
}
|
||||
|
||||
private suspend fun storeInRuntime(userWalletId: UserWalletId, status: PaymentAccountStatus) {
|
||||
runtimeStore.update(default = emptyMap()) { stored ->
|
||||
stored.toMutableMap().apply {
|
||||
put(key = userWalletId.stringValue, value = status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatus) {
|
||||
val statusDM = PaymentAccountStatusDMConverter.convert(value = status) ?: return
|
||||
persistenceDataStore.updateData { storedStatuses ->
|
||||
storedStatuses.toMutableMap().apply {
|
||||
put(key = userWalletId.stringValue, value = statusDM)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue