Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-05 19:47:33 +05:00
commit 59e90854e6
22 changed files with 81 additions and 49 deletions

View file

@ -118,6 +118,17 @@
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="onramp-success"
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

View file

@ -208,7 +208,7 @@ internal class ChildFactory @Inject constructor(
is AppRoute.OnrampSuccess -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnrampSuccessComponent.Params(route.txId),
params = OnrampSuccessComponent.Params(route.externalTxId),
componentFactory = onrampSuccessComponentFactory,
)
}

View file

@ -306,8 +306,8 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class OnrampSuccess(
val txId: String,
) : AppRoute(path = "/onramp/success/$txId"), RouteBundleParams {
val externalTxId: String,
) : AppRoute(path = "/onramp/success/$externalTxId"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}

View file

@ -2,13 +2,25 @@ package com.tangem.core.deeplink.global
import com.tangem.core.deeplink.DeepLink
class BuyCurrencyDeepLink(val onReceive: (txId: String) -> Unit) : DeepLink {
class BuyCurrencyDeepLink(
isOnrampFeatureEnabled: Boolean,
val onReceive: (externalTxId: String) -> Unit,
) : DeepLink {
override val uri: String = "tangem://redirect?action=dismissBrowser"
override val uri: String = if (isOnrampFeatureEnabled) {
ONRAMP_REDIRECT_DEEPLINK
} else {
BUY_REDIRECT_DEEPLINK
}
override fun onReceive(params: Map<String, String>) {
onReceive(
params["txId"] ?: return,
params["merchant_transaction_id"] ?: return,
)
}
companion object {
const val ONRAMP_REDIRECT_DEEPLINK = "tangem://onramp-success?"
private const val BUY_REDIRECT_DEEPLINK = "tangem://redirect?action=dismissBrowser"
}
}

View file

@ -14,6 +14,7 @@ dependencies {
/** Core modules */
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.core.deepLinks.global)
/** Common modules */
implementation(projects.data.common)

View file

@ -2,6 +2,7 @@ package com.tangem.data.onramp
import com.squareup.moshi.Moshi
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.onramp.converters.CountryConverter
import com.tangem.data.onramp.converters.CurrencyConverter
@ -283,7 +284,7 @@ internal class DefaultOnrampRepository(
toDecimals = cryptoCurrency.decimals,
providerId = quote.provider.id,
toAddress = address,
redirectUrl = "tangem://redirect?action=dismissBrowser",
redirectUrl = BuyCurrencyDeepLink.ONRAMP_REDIRECT_DEEPLINK,
language = null,
theme = getTheme(),
requestId = requestId,

View file

@ -32,7 +32,7 @@ internal class DefaultOnrampTransactionRepository(
)?.map(transactionConverter::convert) ?: mutableSetOf()
val updated = stored.toMutableSet()
.addOrReplace(transaction) { it.txId == transaction.txId }
.addOrReplace(transaction) { it.externalTxId == transaction.externalTxId }
.map(transactionConverter::convertBack)
.toSet()
@ -44,12 +44,12 @@ internal class DefaultOnrampTransactionRepository(
}
}
override suspend fun getTransactionById(txId: String): OnrampTransaction? = withContext(dispatchers.io) {
override suspend fun getTransactionById(externalTxId: String): OnrampTransaction? = withContext(dispatchers.io) {
val stored = appPreferencesStore.getObjectSetSync<OnrampTransactionDTO>(
PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
).map(transactionConverter::convert)
stored.firstOrNull { it.txId == txId }
stored.firstOrNull { it.externalTxId == externalTxId }
}
override fun getTransactions(
@ -63,13 +63,13 @@ internal class DefaultOnrampTransactionRepository(
}.map(transactionConverter::convert)
}
override suspend fun updateTransactionStatus(txId: String, status: OnrampStatus.Status) =
override suspend fun updateTransactionStatus(externalTxId: String, status: OnrampStatus.Status) =
withContext(dispatchers.io) {
val updatedTx = getTransactionById(txId)?.copy(status = status) ?: return@withContext
val updatedTx = getTransactionById(externalTxId)?.copy(status = status) ?: return@withContext
storeTransaction(updatedTx)
}
override suspend fun removeTransaction(txId: String) {
override suspend fun removeTransaction(externalTxId: String) {
withContext(dispatchers.io) {
appPreferencesStore.editData { mutablePreferences ->
runCatching {
@ -77,7 +77,7 @@ internal class DefaultOnrampTransactionRepository(
PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
)?.toMutableSet()
stored?.removeIf { it.txId == txId }
stored?.removeIf { it.externalTxId == externalTxId }
mutablePreferences.setObjectSet<OnrampTransactionDTO>(
key = PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,

View file

@ -11,9 +11,9 @@ class GetOnrampStatusUseCase(
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(txId: String): Either<OnrampError, OnrampStatus> {
suspend operator fun invoke(externalTxId: String): Either<OnrampError, OnrampStatus> {
return Either.catch {
onrampRepository.getStatus(txId)
onrampRepository.getStatus(externalTxId)
}.mapLeft {
errorResolver.resolve(it)
}

View file

@ -9,10 +9,10 @@ class GetOnrampTransactionUseCase(
private val onrampTransactionRepository: OnrampTransactionRepository,
) {
suspend operator fun invoke(txId: String): Either<OnrampError, OnrampTransaction> {
suspend operator fun invoke(externalTxId: String): Either<OnrampError, OnrampTransaction> {
return Either.catch {
requireNotNull(
onrampTransactionRepository.getTransactionById(txId),
onrampTransactionRepository.getTransactionById(externalTxId),
)
}.mapLeft {
OnrampError.UnknownError

View file

@ -1,6 +1,7 @@
package com.tangem.domain.onramp
import arrow.core.Either
import arrow.core.left
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
@ -8,9 +9,11 @@ class OnrampRemoveTransactionUseCase(
private val onrampTransactionRepository: OnrampTransactionRepository,
) {
suspend operator fun invoke(txId: String): Either<OnrampError, Unit> {
suspend operator fun invoke(externalTxId: String?): Either<OnrampError, Unit> {
if (externalTxId == null) return OnrampError.UnknownError.left()
return Either.catch {
onrampTransactionRepository.removeTransaction(txId)
onrampTransactionRepository.removeTransaction(externalTxId)
}.mapLeft {
OnrampError.UnknownError
}

View file

@ -8,7 +8,7 @@ class OnrampUpdateTransactionStatusUseCase(
private val onrampTransactionRepository: OnrampTransactionRepository,
) {
suspend operator fun invoke(txId: String, status: OnrampStatus.Status) = Either.catch {
onrampTransactionRepository.updateTransactionStatus(txId = txId, status = status)
suspend operator fun invoke(externalTxId: String, status: OnrampStatus.Status) = Either.catch {
onrampTransactionRepository.updateTransactionStatus(externalTxId = externalTxId, status = status)
}
}

View file

@ -10,11 +10,11 @@ interface OnrampTransactionRepository {
suspend fun storeTransaction(transaction: OnrampTransaction)
suspend fun getTransactionById(txId: String): OnrampTransaction?
suspend fun getTransactionById(externalTxId: String): OnrampTransaction?
fun getTransactions(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow<List<OnrampTransaction>>
suspend fun updateTransactionStatus(txId: String, status: OnrampStatus.Status)
suspend fun updateTransactionStatus(externalTxId: String, status: OnrampStatus.Status)
suspend fun removeTransaction(txId: String)
suspend fun removeTransaction(externalTxId: String)
}

View file

@ -5,7 +5,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
interface OnrampSuccessComponent : ComposableContentComponent {
data class Params(val txId: String)
data class Params(val externalTxId: String)
interface Factory : ComponentFactory<Params, OnrampSuccessComponent>
}

View file

@ -54,7 +54,7 @@ internal class OnrampSuccessComponentModel @Inject constructor(
private fun loadData() {
modelScope.launch {
getOnrampTransactionUseCase(txId = params.txId)
getOnrampTransactionUseCase(externalTxId = params.externalTxId)
.fold(
ifLeft = {
Timber.e(it.toString())
@ -72,7 +72,7 @@ internal class OnrampSuccessComponentModel @Inject constructor(
transaction.toCurrencyId,
).getOrElse { error("Crypto currency not found") }
getOnrampStatusUseCase(txId = params.txId)
getOnrampStatusUseCase(externalTxId = params.externalTxId)
.fold(
ifLeft = {
Timber.e(it.toString())

View file

@ -49,7 +49,7 @@ internal class DefaultTokenDetailsRouter(
)
}
override fun openOnrampSuccess(txId: String) {
router.push(AppRoute.OnrampSuccess(txId))
override fun openOnrampSuccess(externalTxId: String) {
router.push(AppRoute.OnrampSuccess(externalTxId))
}
}

View file

@ -20,5 +20,5 @@ internal interface InnerTokenDetailsRouter : TokenDetailsRouter {
fun openStaking(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, yield: Yield)
fun openOnrampSuccess(txId: String)
fun openOnrampSuccess(externalTxId: String)
}

View file

@ -74,7 +74,7 @@ internal class OnrampStatusFactory @AssistedInject constructor(
val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.OnrampUM ?: return
if (selectedTx.activeStatus.isTerminal) {
onrampRemoveTransactionUseCase(txId = selectedTx.info.txId)
onrampRemoveTransactionUseCase(externalTxId = selectedTx.info.txExternalId)
}
}
@ -97,14 +97,14 @@ internal class OnrampStatusFactory @AssistedInject constructor(
private suspend fun List<ExpressTransactionStateUM.OnrampUM>.clearHiddenTerminal() {
this.filter { it.activeStatus.isHidden && it.activeStatus.isTerminal }
.forEach { onrampRemoveTransactionUseCase(txId = it.info.txId) }
.forEach { onrampRemoveTransactionUseCase(externalTxId = it.info.txExternalId) }
}
private suspend fun sendStatusUpdateAnalytics(
onrampTx: ExpressTransactionStateUM.OnrampUM,
statusModel: OnrampStatus,
) {
val txId = statusModel.txId
val externalTxId = statusModel.externalTxId
val status = toAnalyticStatus(statusModel.status) ?: return
if (statusModel.status != onrampTx.activeStatus) {
@ -116,7 +116,7 @@ internal class OnrampStatusFactory @AssistedInject constructor(
fiatCurrency = onrampTx.fromCurrencyCode,
),
)
onrampUpdateTransactionStatusUseCase(txId, statusModel.status)
onrampUpdateTransactionStatusUseCase(externalTxId = externalTxId, statusModel.status)
}
}

View file

@ -189,15 +189,18 @@ internal class TokenDetailsViewModel @Inject constructor(
deepLinksRegistry.registerWithViewModel(
viewModel = this,
deepLinks = listOf(
BuyCurrencyDeepLink(::onBuyCurrencyDeepLink),
BuyCurrencyDeepLink(
isOnrampFeatureEnabled = onrampFeatureToggles.isFeatureEnabled,
onReceive = ::onBuyCurrencyDeepLink,
),
),
)
userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: error("UserWallet not found")
}
private fun onBuyCurrencyDeepLink(txId: String) {
private fun onBuyCurrencyDeepLink(externalTxId: String) {
if (onrampFeatureToggles.isFeatureEnabled) {
router.openOnrampSuccess(txId)
router.openOnrampSuccess(externalTxId)
} else {
val currency = cryptoCurrencyStatus?.currency ?: return
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Bought(currency.symbol))

View file

@ -65,8 +65,9 @@ internal class WalletDeepLinksHandler @Inject constructor(
if (onrampFeatureToggles.isFeatureEnabled || !userWallet.isMultiCurrency) {
add(
BuyCurrencyDeepLink(
onReceive = { txId ->
scope.launch { onBuyCurrencyDeepLink(txId, userWallet) }
isOnrampFeatureEnabled = onrampFeatureToggles.isFeatureEnabled,
onReceive = { externalTxId ->
scope.launch { onBuyCurrencyDeepLink(externalTxId, userWallet) }
},
),
)
@ -106,9 +107,9 @@ internal class WalletDeepLinksHandler @Inject constructor(
}
}
private suspend fun onBuyCurrencyDeepLink(txId: String, userWallet: UserWallet) {
private suspend fun onBuyCurrencyDeepLink(externalTxId: String, userWallet: UserWallet) {
if (onrampFeatureToggles.isFeatureEnabled) {
clickIntents.onOnrampSuccessClick(txId)
clickIntents.onOnrampSuccessClick(externalTxId)
} else {
val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId).getOrNull() ?: return
analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol))

View file

@ -129,8 +129,8 @@ internal class DefaultWalletRouter(
}
}
override fun openOnrampSuccessScreen(txId: String) {
router.push(AppRoute.OnrampSuccess(txId))
override fun openOnrampSuccessScreen(externalTxId: String) {
router.push(AppRoute.OnrampSuccess(externalTxId))
}
override fun openUrl(url: String) {

View file

@ -39,8 +39,8 @@ internal interface InnerWalletRouter : WalletRouter {
/** Open onboarding screen */
fun openOnboardingScreen(scanResponse: ScanResponse, continueBackup: Boolean = false)
/** Open onramp success screen for [txId] */
fun openOnrampSuccessScreen(txId: String)
/** Open onramp success screen for [externalTxId] */
fun openOnrampSuccessScreen(externalTxId: String)
/** Open transaction history website by [url] */
fun openUrl(url: String)

View file

@ -34,7 +34,7 @@ internal interface WalletContentClickIntents {
fun onOrganizeTokensClick()
fun onOnrampSuccessClick(txId: String)
fun onOnrampSuccessClick(externalTxId: String)
fun onDismissMarketsOnboarding()
@ -99,8 +99,8 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId())
}
override fun onOnrampSuccessClick(txId: String) {
router.openOnrampSuccessScreen(txId = txId)
override fun onOnrampSuccessClick(externalTxId: String) {
router.openOnrampSuccessScreen(externalTxId = externalTxId)
}
override fun onDismissMarketsOnboarding() {