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_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 ONBOARDING_FINALIZE_SCAN_RESPONSE_KEY by lazy { stringPreferencesKey(name = "onboardingFinalizeScanResponse") }
|
||||||
|
|
||||||
val IS_GOOGLE_SERVICES_AVAILABLE_KEY by lazy { booleanPreferencesKey(name = "isGoogleServicesAvailable") }
|
val IS_GOOGLE_SERVICES_AVAILABLE_KEY by lazy { booleanPreferencesKey(name = "isGoogleServicesAvailable") }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.data.onramp
|
package com.tangem.data.onramp
|
||||||
|
|
||||||
import com.tangem.data.onramp.converters.TransactionConverter
|
import com.tangem.data.onramp.converters.TransactionConverter
|
||||||
|
import com.tangem.data.onramp.models.OnrampTerminalTransactionDTO
|
||||||
import com.tangem.data.onramp.models.OnrampTransactionDTO
|
import com.tangem.data.onramp.models.OnrampTransactionDTO
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||||
|
|
@ -15,7 +16,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import com.tangem.utils.extensions.addOrReplace
|
import com.tangem.utils.extensions.addOrReplace
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
internal class DefaultOnrampTransactionRepository(
|
internal class DefaultOnrampTransactionRepository(
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
|
|
@ -47,6 +50,7 @@ internal class DefaultOnrampTransactionRepository(
|
||||||
override fun getAllTransactions(): Flow<List<OnrampTransaction>> {
|
override fun getAllTransactions(): Flow<List<OnrampTransaction>> {
|
||||||
return appPreferencesStore
|
return appPreferencesStore
|
||||||
.getObjectSet<OnrampTransactionDTO>(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY)
|
.getObjectSet<OnrampTransactionDTO>(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY)
|
||||||
|
.onStart { cleanupExpiredTerminalTransactions() }
|
||||||
.map { transactions -> transactions.map(transactionConverter::convert) }
|
.map { transactions -> transactions.map(transactionConverter::convert) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,6 +67,7 @@ internal class DefaultOnrampTransactionRepository(
|
||||||
cryptoCurrencyId: CryptoCurrency.ID,
|
cryptoCurrencyId: CryptoCurrency.ID,
|
||||||
): Flow<List<OnrampTransaction>> = appPreferencesStore
|
): Flow<List<OnrampTransaction>> = appPreferencesStore
|
||||||
.getObjectSet<OnrampTransactionDTO>(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY)
|
.getObjectSet<OnrampTransactionDTO>(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY)
|
||||||
|
.onStart { cleanupExpiredTerminalTransactions() }
|
||||||
.map { transactions ->
|
.map { transactions ->
|
||||||
transactions.filter {
|
transactions.filter {
|
||||||
it.userWalletId == userWalletId && it.toCurrencyId == cryptoCurrencyId.value
|
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()
|
) : OnrampError()
|
||||||
|
|
||||||
data object PairsNotFound : OnrampError()
|
data object PairsNotFound : OnrampError()
|
||||||
|
|
||||||
|
data object AlreadyHandledTransaction : OnrampError()
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.domain.onramp
|
package com.tangem.domain.onramp
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
|
import arrow.core.left
|
||||||
import com.tangem.domain.onramp.model.cache.OnrampTransaction
|
import com.tangem.domain.onramp.model.cache.OnrampTransaction
|
||||||
import com.tangem.domain.onramp.model.error.OnrampError
|
import com.tangem.domain.onramp.model.error.OnrampError
|
||||||
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
||||||
|
|
@ -12,6 +13,10 @@ class GetOnrampTransactionUseCase(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend operator fun invoke(txId: String): Either<OnrampError, OnrampTransaction> {
|
suspend operator fun invoke(txId: String): Either<OnrampError, OnrampTransaction> {
|
||||||
|
if (onrampTransactionRepository.isHandledTransaction(txId)) {
|
||||||
|
return OnrampError.AlreadyHandledTransaction.left()
|
||||||
|
}
|
||||||
|
|
||||||
return Either.catch {
|
return Either.catch {
|
||||||
requireNotNull(
|
requireNotNull(
|
||||||
onrampTransactionRepository.getTransactionById(txId),
|
onrampTransactionRepository.getTransactionById(txId),
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,13 @@ class OnrampRemoveTransactionUseCase(
|
||||||
private val errorResolver: OnrampErrorResolver,
|
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()
|
if (txId == null) return OnrampError.DomainError("Transaction id not provided").left()
|
||||||
|
|
||||||
return Either.catch {
|
return Either.catch {
|
||||||
|
if (!forceRemove) {
|
||||||
|
onrampTransactionRepository.storeHandledTransaction(txId)
|
||||||
|
}
|
||||||
onrampTransactionRepository.removeTransaction(txId)
|
onrampTransactionRepository.removeTransaction(txId)
|
||||||
}.mapLeft(errorResolver::resolve)
|
}.mapLeft(errorResolver::resolve)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,8 @@ interface OnrampTransactionRepository {
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun removeTransaction(txId: String)
|
suspend fun removeTransaction(txId: String)
|
||||||
|
|
||||||
|
suspend fun storeHandledTransaction(txId: String)
|
||||||
|
|
||||||
|
suspend fun isHandledTransaction(txId: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
@ -111,6 +111,7 @@ internal class AllOffersStateFactory(
|
||||||
-> true
|
-> true
|
||||||
is OnrampError.AmountError,
|
is OnrampError.AmountError,
|
||||||
is OnrampError.RedirectError,
|
is OnrampError.RedirectError,
|
||||||
|
is OnrampError.AlreadyHandledTransaction,
|
||||||
-> false
|
-> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ internal class OnrampStateFactory(
|
||||||
is OnrampError.AmountError.TooSmallError,
|
is OnrampError.AmountError.TooSmallError,
|
||||||
OnrampError.RedirectError.VerificationFailed,
|
OnrampError.RedirectError.VerificationFailed,
|
||||||
OnrampError.RedirectError.WrongRequestId,
|
OnrampError.RedirectError.WrongRequestId,
|
||||||
|
OnrampError.AlreadyHandledTransaction,
|
||||||
-> currentStateProvider() // ignore error state
|
-> currentStateProvider() // ignore error state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ internal class OnrampV2StateFactory(
|
||||||
is OnrampError.AmountError.TooSmallError,
|
is OnrampError.AmountError.TooSmallError,
|
||||||
OnrampError.RedirectError.VerificationFailed,
|
OnrampError.RedirectError.VerificationFailed,
|
||||||
OnrampError.RedirectError.WrongRequestId,
|
OnrampError.RedirectError.WrongRequestId,
|
||||||
|
OnrampError.AlreadyHandledTransaction,
|
||||||
-> currentStateProvider() // ignore error state
|
-> currentStateProvider() // ignore error state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -177,10 +177,12 @@ internal class OnrampSuccessComponentModel @Inject constructor(
|
||||||
private fun showErrorAlert(error: OnrampError) {
|
private fun showErrorAlert(error: OnrampError) {
|
||||||
val errorCode = (error as? OnrampError.DataError)?.code
|
val errorCode = (error as? OnrampError.DataError)?.code
|
||||||
val message = DialogMessage(
|
val message = DialogMessage(
|
||||||
message = if (errorCode.isNullOrBlank()) {
|
message = when {
|
||||||
resourceReference(R.string.common_unknown_error)
|
!errorCode.isNullOrBlank() -> resourceReference(R.string.express_error_code, wrappedList(errorCode))
|
||||||
} else {
|
error is OnrampError.AlreadyHandledTransaction -> resourceReference(
|
||||||
resourceReference(R.string.express_error_code, wrappedList(errorCode))
|
R.string.onramp_error_transaction_already_processed,
|
||||||
|
)
|
||||||
|
else -> resourceReference(R.string.common_unknown_error)
|
||||||
},
|
},
|
||||||
firstActionBuilder = {
|
firstActionBuilder = {
|
||||||
okAction(router::pop)
|
okAction(router::pop)
|
||||||
|
|
|
||||||
|
|
@ -31,5 +31,6 @@ internal fun AnalyticsEventHandler.sendOnrampErrorEvent(
|
||||||
OnrampError.RedirectError.VerificationFailed,
|
OnrampError.RedirectError.VerificationFailed,
|
||||||
OnrampError.RedirectError.WrongRequestId,
|
OnrampError.RedirectError.WrongRequestId,
|
||||||
OnrampError.PairsNotFound,
|
OnrampError.PairsNotFound,
|
||||||
|
OnrampError.AlreadyHandledTransaction,
|
||||||
-> { /* no-op */ }
|
-> { /* no-op */ }
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue