Updated on 2026-08-14
This commit is contained in:
parent
0ccfceba04
commit
35e0cccc90
12 changed files with 110 additions and 5 deletions
|
|
@ -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") }
|
||||
|
|
|
|||
|
|
@ -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<List<OnrampTransaction>> {
|
||||
return appPreferencesStore
|
||||
.getObjectSet<OnrampTransactionDTO>(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY)
|
||||
.onStart { cleanupExpiredTerminalTransactions() }
|
||||
.map { transactions -> transactions.map(transactionConverter::convert) }
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +67,7 @@ internal class DefaultOnrampTransactionRepository(
|
|||
cryptoCurrencyId: CryptoCurrency.ID,
|
||||
): Flow<List<OnrampTransaction>> = appPreferencesStore
|
||||
.getObjectSet<OnrampTransactionDTO>(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<OnrampTerminalTransactionDTO>(
|
||||
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<OnrampTerminalTransactionDTO>(
|
||||
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<OnrampTerminalTransactionDTO>(
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -31,4 +31,6 @@ sealed class OnrampError {
|
|||
) : OnrampError()
|
||||
|
||||
data object PairsNotFound : OnrampError()
|
||||
|
||||
data object AlreadyHandledTransaction : OnrampError()
|
||||
}
|
||||
|
|
@ -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<OnrampError, OnrampTransaction> {
|
||||
if (onrampTransactionRepository.isHandledTransaction(txId)) {
|
||||
return OnrampError.AlreadyHandledTransaction.left()
|
||||
}
|
||||
|
||||
return Either.catch {
|
||||
requireNotNull(
|
||||
onrampTransactionRepository.getTransactionById(txId),
|
||||
|
|
|
|||
|
|
@ -11,10 +11,13 @@ class OnrampRemoveTransactionUseCase(
|
|||
private val errorResolver: OnrampErrorResolver,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(txId: String?): Either<OnrampError, Unit> {
|
||||
suspend operator fun invoke(txId: String?, forceRemove: Boolean = false): Either<OnrampError, Unit> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,4 +24,8 @@ interface OnrampTransactionRepository {
|
|||
)
|
||||
|
||||
suspend fun removeTransaction(txId: String)
|
||||
|
||||
suspend fun storeHandledTransaction(txId: String)
|
||||
|
||||
suspend fun isHandledTransaction(txId: String): Boolean
|
||||
}
|
||||
|
|
@ -111,6 +111,7 @@ internal class AllOffersStateFactory(
|
|||
-> true
|
||||
is OnrampError.AmountError,
|
||||
is OnrampError.RedirectError,
|
||||
is OnrampError.AlreadyHandledTransaction,
|
||||
-> false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ internal class OnrampStateFactory(
|
|||
is OnrampError.AmountError.TooSmallError,
|
||||
OnrampError.RedirectError.VerificationFailed,
|
||||
OnrampError.RedirectError.WrongRequestId,
|
||||
OnrampError.AlreadyHandledTransaction,
|
||||
-> currentStateProvider() // ignore error state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ internal class OnrampV2StateFactory(
|
|||
is OnrampError.AmountError.TooSmallError,
|
||||
OnrampError.RedirectError.VerificationFailed,
|
||||
OnrampError.RedirectError.WrongRequestId,
|
||||
OnrampError.AlreadyHandledTransaction,
|
||||
-> currentStateProvider() // ignore error state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -31,5 +31,6 @@ internal fun AnalyticsEventHandler.sendOnrampErrorEvent(
|
|||
OnrampError.RedirectError.VerificationFailed,
|
||||
OnrampError.RedirectError.WrongRequestId,
|
||||
OnrampError.PairsNotFound,
|
||||
OnrampError.AlreadyHandledTransaction,
|
||||
-> { /* no-op */ }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue