Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-01 11:50:47 +03:00
parent 1edf995ab0
commit 2dadb60975
26 changed files with 1110 additions and 48 deletions

View file

@ -0,0 +1,18 @@
package com.tangem.data.pay
import com.tangem.data.pay.store.PaymentAccountStatusesStore
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
import com.tangem.domain.common.wallets.UserWalletDataCleaner
import com.tangem.domain.models.wallet.UserWalletId
import javax.inject.Inject
internal class TangemPayUserWalletDataCleaner @Inject constructor(
private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
private val txHistoryItemsStore: TangemPayTxHistoryItemsStore,
) : UserWalletDataCleaner {
override suspend fun clear(userWalletIds: List<UserWalletId>) {
paymentAccountStatusesStore.remove(userWalletIds)
txHistoryItemsStore.remove(userWalletIds.map { it.stringValue })
}
}

View file

@ -6,6 +6,7 @@ import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.data.pay.DefaultTangemPayEligibilityManager
import com.tangem.data.pay.TangemPayUserWalletDataCleaner
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter
import com.tangem.data.pay.entity.DefaultTangemPayCurrencyFactory
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher
@ -21,6 +22,7 @@ import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.domain.common.wallets.UserWalletDataCleaner
import com.tangem.domain.pay.TangemPayCurrencyFactory
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
@ -41,6 +43,7 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import javax.inject.Singleton
@Module
@ -117,6 +120,10 @@ internal interface TangemPayDataModule {
@Singleton
fun bindPaymentAccountStatusFetcher(impl: DefaultPaymentAccountStatusFetcher): PaymentAccountStatusFetcher
@Binds
@IntoSet
fun bindTangemPayUserWalletDataCleaner(impl: TangemPayUserWalletDataCleaner): UserWalletDataCleaner
companion object {
@Provides

View file

@ -16,6 +16,7 @@ import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
import com.tangem.datasource.api.pay.models.response.OrderResponse
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
@ -49,6 +50,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
private val cardFrozenStateStore: TangemPayCardFrozenStateStore,
private val userWalletsListRepository: UserWalletsListRepository,
private val paymentAccountStatusStore: PaymentAccountStatusesStore,
private val txHistoryItemsStore: TangemPayTxHistoryItemsStore,
) : OnboardingRepository {
// Save data for a session
@ -271,6 +273,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
}.map {
val address = requestHelper.getCustomerWalletAddress(userWalletId)
tangemPayStorage.clearAll(userWalletId = userWalletId, customerWalletAddress = address)
txHistoryItemsStore.remove(userWalletId.stringValue)
setHideMainOnboardingBanner(userWalletId)
}
}

View file

@ -18,6 +18,7 @@ import com.tangem.pagination.fetcher.BatchFetcher
import com.tangem.pagination.fetcher.CursorBatchFetcher
import com.tangem.pagination.toBatchFlow
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import javax.inject.Inject
private const val INITIAL_CURSOR = "initial_cursor_key"
@ -76,16 +77,22 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
cursor: String?,
limit: Int,
): List<TangemPayTxHistoryItem> {
cacheRegistry.invokeOnExpire(
key = getCacheKey(userWalletId = userWalletId, cursor = cursor),
skipCache = config.shouldRefresh,
block = { fetch(userWalletId = userWalletId, cursor = cursor, pageSize = limit) },
)
val storeKey = userWalletId.stringValue
val storeCursor = cursor ?: INITIAL_CURSOR
return txHistoryItemsStore.getSyncOrNull(
key = userWalletId.stringValue,
cursor = cursor ?: INITIAL_CURSOR,
).orEmpty()
val fetchResult = runSuspendCatching {
cacheRegistry.invokeOnExpire(
key = getCacheKey(userWalletId = userWalletId, cursor = cursor),
skipCache = config.shouldRefresh,
block = { fetch(userWalletId = userWalletId, cursor = cursor, pageSize = limit) },
)
}
val storedPage = txHistoryItemsStore.getSyncOrNull(key = storeKey, cursor = storeCursor)
return fetchResult.fold(
onSuccess = { storedPage.orEmpty() },
onFailure = { e -> storedPage?.takeIf { it.isNotEmpty() } ?: throw e },
)
}
private fun getCacheKey(userWalletId: UserWalletId, cursor: String?): String {

View file

@ -101,6 +101,20 @@ internal class PaymentAccountStatusesStore(
return runtimeStore.getSyncOrDefault(emptyMap()).containsKey(userWalletId.stringValue)
}
suspend fun remove(userWalletId: UserWalletId) {
logger.i("remove($userWalletId)")
remove(userWalletIds = listOf(userWalletId))
}
suspend fun remove(userWalletIds: List<UserWalletId>) {
logger.i("remove($userWalletIds)")
val keys = userWalletIds.map { it.stringValue }.toSet()
coroutineScope {
launch { runtimeStore.update(default = emptyMap()) { it - keys } }
launch { persistenceDataStore.updateData { it - keys } }
}
}
private suspend fun storeInRuntime(userWalletId: UserWalletId, status: AccountStatus.Payment) {
runtimeStore.update(default = emptyMap()) { stored ->
stored.toMutableMap().apply {

View file

@ -0,0 +1,49 @@
package com.tangem.data.pay
import com.tangem.data.pay.store.PaymentAccountStatusesStore
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.Runs
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.just
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class TangemPayUserWalletDataCleanerTest {
private val paymentAccountStatusesStore: PaymentAccountStatusesStore = mockk()
private val txHistoryItemsStore: TangemPayTxHistoryItemsStore = mockk()
private val cleaner = TangemPayUserWalletDataCleaner(
paymentAccountStatusesStore = paymentAccountStatusesStore,
txHistoryItemsStore = txHistoryItemsStore,
)
@BeforeEach
fun resetMocks() {
clearMocks(paymentAccountStatusesStore, txHistoryItemsStore)
coEvery { paymentAccountStatusesStore.remove(any<List<UserWalletId>>()) } just Runs
coEvery { txHistoryItemsStore.remove(any<List<String>>()) } just Runs
}
@Test
fun `GIVEN wallets WHEN clear THEN each store is cleared once with all ids in a single call`() = runTest {
// Act
cleaner.clear(listOf(WALLET_A, WALLET_B))
// Assert
coVerify(exactly = 1) { paymentAccountStatusesStore.remove(listOf(WALLET_A, WALLET_B)) }
coVerify(exactly = 1) { txHistoryItemsStore.remove(listOf(WALLET_A.stringValue, WALLET_B.stringValue)) }
}
private companion object {
val WALLET_A = UserWalletId("011")
val WALLET_B = UserWalletId("022")
}
}

View file

@ -0,0 +1,89 @@
package com.tangem.data.pay.store
import com.google.common.truth.Truth.assertThat
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayCurrencyFactory
import com.tangem.test.core.TestAppCoroutineScope
import com.tangem.test.core.datastore.MockStateDataStore
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
internal class PaymentAccountStatusesStoreTest {
@Test
fun `GIVEN stored status WHEN remove single THEN cleared from both runtime and persistence`() = runTest {
// Arrange
val persistenceDataStore = MockStateDataStore<WalletIdWithPaymentStatusDM>(default = emptyMap())
val store = createStore(persistenceDataStore)
advanceUntilIdle()
store.storeNotCreated(WALLET_A)
advanceUntilIdle()
// sanity: present in both stores before removal
assertThat(store.getSyncOrNull(WALLET_A)).isNotNull()
assertThat(persistenceDataStore.data.first()).containsKey(WALLET_A.stringValue)
// Act
store.remove(WALLET_A)
advanceUntilIdle()
// Assert
assertThat(store.getSyncOrNull(WALLET_A)).isNull()
assertThat(persistenceDataStore.data.first()).doesNotContainKey(WALLET_A.stringValue)
}
@Test
fun `GIVEN statuses for two wallets WHEN remove list THEN both cleared from both stores`() = runTest {
// Arrange
val persistenceDataStore = MockStateDataStore<WalletIdWithPaymentStatusDM>(default = emptyMap())
val store = createStore(persistenceDataStore)
advanceUntilIdle()
store.storeNotCreated(WALLET_A)
store.storeNotCreated(WALLET_B)
advanceUntilIdle()
// Act
store.remove(listOf(WALLET_A, WALLET_B))
advanceUntilIdle()
// Assert
assertThat(store.getSyncOrNull(WALLET_A)).isNull()
assertThat(store.getSyncOrNull(WALLET_B)).isNull()
assertThat(persistenceDataStore.data.first()).doesNotContainKey(WALLET_A.stringValue)
assertThat(persistenceDataStore.data.first()).doesNotContainKey(WALLET_B.stringValue)
}
private fun TestScope.createStore(
persistenceDataStore: MockStateDataStore<WalletIdWithPaymentStatusDM>,
) = PaymentAccountStatusesStore(
runtimeStore = RuntimeSharedStore<WalletIdWithPaymentStatus>(),
persistenceDataStore = persistenceDataStore,
converter = PaymentAccountStatusValueDMConverter(mockk<TangemPayCurrencyFactory>(relaxed = true)),
scope = TestAppCoroutineScope(this),
)
private suspend fun PaymentAccountStatusesStore.storeNotCreated(userWalletId: UserWalletId) {
store(
userWalletId = userWalletId,
status = AccountStatus.Payment(
account = Account.Payment(userWalletId),
value = PaymentAccountStatusValue.NotCreated,
),
)
}
private companion object {
val WALLET_A = UserWalletId("011")
val WALLET_B = UserWalletId("022")
}
}