diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 775e916143..3fafa4462a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -108,6 +108,8 @@ object PreferencesKeys { val ONRAMP_TRANSACTIONS_STATUSES_KEY by lazy { stringPreferencesKey(name = "onrampTransactionsStatuses") } + val ONRAMP_HANDLED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "onrampHandledTransactions") } + val ONBOARDING_FINALIZE_SCAN_RESPONSE_KEY by lazy { stringPreferencesKey(name = "onboardingFinalizeScanResponse") } val IS_GOOGLE_SERVICES_AVAILABLE_KEY by lazy { booleanPreferencesKey(name = "isGoogleServicesAvailable") } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt index 5e515100e3..782b4b7b5b 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.onramp import com.tangem.data.onramp.converters.TransactionConverter +import com.tangem.data.onramp.models.OnrampTerminalTransactionDTO import com.tangem.data.onramp.models.OnrampTransactionDTO import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -15,7 +16,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext +import java.util.concurrent.TimeUnit internal class DefaultOnrampTransactionRepository( private val appPreferencesStore: AppPreferencesStore, @@ -47,6 +50,7 @@ internal class DefaultOnrampTransactionRepository( override fun getAllTransactions(): Flow> { return appPreferencesStore .getObjectSet(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY) + .onStart { cleanupExpiredTerminalTransactions() } .map { transactions -> transactions.map(transactionConverter::convert) } } @@ -63,6 +67,7 @@ internal class DefaultOnrampTransactionRepository( cryptoCurrencyId: CryptoCurrency.ID, ): Flow> = appPreferencesStore .getObjectSet(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY) + .onStart { cleanupExpiredTerminalTransactions() } .map { transactions -> transactions.filter { it.userWalletId == userWalletId && it.toCurrencyId == cryptoCurrencyId.value @@ -101,4 +106,64 @@ internal class DefaultOnrampTransactionRepository( } } } + + override suspend fun isHandledTransaction(txId: String): Boolean = withContext(dispatchers.io) { + val terminated = appPreferencesStore.getObjectSetSync( + PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + ) + + terminated.any { it.txId == txId } + } + + override suspend fun storeHandledTransaction(txId: String) { + withContext(dispatchers.io) { + appPreferencesStore.editData { mutablePreferences -> + runCatching { + val archived = mutablePreferences.getObjectSet( + PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + ).orEmpty().toMutableSet() + + val record = OnrampTerminalTransactionDTO( + txId = txId, + terminatedAt = System.currentTimeMillis(), + ) + + archived.add(record) + + mutablePreferences.setObjectSet( + key = PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + value = archived, + ) + } + } + } + } + + private suspend fun cleanupExpiredTerminalTransactions() { + withContext(dispatchers.io) { + appPreferencesStore.editData { mutablePreferences -> + runCatching { + val archived = mutablePreferences.getObjectSet( + PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + )?.toMutableSet() ?: return@editData + + val oneWeekAgo = System.currentTimeMillis() - + TimeUnit.DAYS.toMillis(HANDLED_TRANSACTIONS_RETENTION_DAYS) + val cleaned = archived + .filterTo(mutableSetOf()) { it.terminatedAt > oneWeekAgo } + + if (cleaned.size != archived.size) { + mutablePreferences.setObjectSet( + key = PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + value = cleaned, + ) + } + } + } + } + } + + private companion object { + const val HANDLED_TRANSACTIONS_RETENTION_DAYS = 7L + } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTerminalTransactionDTO.kt b/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTerminalTransactionDTO.kt new file mode 100644 index 0000000000..46aae3c528 --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTerminalTransactionDTO.kt @@ -0,0 +1,18 @@ +package com.tangem.data.onramp.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Model for terminal onramp transaction + * + * @property txId Transaction ID + * @property terminatedAt Termination timestamp in milliseconds + */ +@JsonClass(generateAdapter = true) +data class OnrampTerminalTransactionDTO( + @Json(name = "txId") + val txId: String, + @Json(name = "terminatedAt") + val terminatedAt: Long, +) \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/error/OnrampError.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/error/OnrampError.kt index cf1f656e93..96e2f710ba 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/error/OnrampError.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/error/OnrampError.kt @@ -31,4 +31,6 @@ sealed class OnrampError { ) : OnrampError() data object PairsNotFound : OnrampError() + + data object AlreadyHandledTransaction : OnrampError() } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt index 72ab25fe7c..2ac28eb227 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.onramp import arrow.core.Either +import arrow.core.left import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.repositories.OnrampErrorResolver @@ -12,6 +13,10 @@ class GetOnrampTransactionUseCase( ) { suspend operator fun invoke(txId: String): Either { + if (onrampTransactionRepository.isHandledTransaction(txId)) { + return OnrampError.AlreadyHandledTransaction.left() + } + return Either.catch { requireNotNull( onrampTransactionRepository.getTransactionById(txId), diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt index 3d94a2562c..2eddae74ac 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt @@ -11,10 +11,13 @@ class OnrampRemoveTransactionUseCase( private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(txId: String?): Either { + suspend operator fun invoke(txId: String?, forceRemove: Boolean = false): Either { if (txId == null) return OnrampError.DomainError("Transaction id not provided").left() return Either.catch { + if (!forceRemove) { + onrampTransactionRepository.storeHandledTransaction(txId) + } onrampTransactionRepository.removeTransaction(txId) }.mapLeft(errorResolver::resolve) } diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt index c0ab3000ce..46cfdcbe6f 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt @@ -24,4 +24,8 @@ interface OnrampTransactionRepository { ) suspend fun removeTransaction(txId: String) + + suspend fun storeHandledTransaction(txId: String) + + suspend fun isHandledTransaction(txId: String): Boolean } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt index 4a29045354..3dcc789481 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt @@ -111,6 +111,7 @@ internal class AllOffersStateFactory( -> true is OnrampError.AmountError, is OnrampError.RedirectError, + is OnrampError.AlreadyHandledTransaction, -> false } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index b0a9d7773a..8faf435657 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -63,6 +63,7 @@ internal class OnrampStateFactory( is OnrampError.AmountError.TooSmallError, OnrampError.RedirectError.VerificationFailed, OnrampError.RedirectError.WrongRequestId, + OnrampError.AlreadyHandledTransaction, -> currentStateProvider() // ignore error state } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt index ad9a4c4577..06ec867696 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt @@ -86,6 +86,7 @@ internal class OnrampV2StateFactory( is OnrampError.AmountError.TooSmallError, OnrampError.RedirectError.VerificationFailed, OnrampError.RedirectError.WrongRequestId, + OnrampError.AlreadyHandledTransaction, -> currentStateProvider() // ignore error state } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 5f318b6a4f..15b4eed93a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -177,10 +177,12 @@ internal class OnrampSuccessComponentModel @Inject constructor( private fun showErrorAlert(error: OnrampError) { val errorCode = (error as? OnrampError.DataError)?.code val message = DialogMessage( - message = if (errorCode.isNullOrBlank()) { - resourceReference(R.string.common_unknown_error) - } else { - resourceReference(R.string.express_error_code, wrappedList(errorCode)) + message = when { + !errorCode.isNullOrBlank() -> resourceReference(R.string.express_error_code, wrappedList(errorCode)) + error is OnrampError.AlreadyHandledTransaction -> resourceReference( + R.string.onramp_error_transaction_already_processed, + ) + else -> resourceReference(R.string.common_unknown_error) }, firstActionBuilder = { okAction(router::pop) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt index e9ec5c76c5..f39254139d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt @@ -31,5 +31,6 @@ internal fun AnalyticsEventHandler.sendOnrampErrorEvent( OnrampError.RedirectError.VerificationFailed, OnrampError.RedirectError.WrongRequestId, OnrampError.PairsNotFound, + OnrampError.AlreadyHandledTransaction, -> { /* no-op */ } } \ No newline at end of file