Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-10 10:20:55 +03:00
commit 53b9f665eb
42 changed files with 382 additions and 53 deletions

View file

@ -18,7 +18,7 @@ class ForegroundActivityObserver : ActivityResultCaller {
val foregroundActivity: AppCompatActivity?
get() = activities.entries
.firstOrNull { it.value?.isDestroyed ?: false }
.firstOrNull { it.value?.isDestroyed == false }
?.value
internal val callbacks: ActivityLifecycleCallbacks

View file

@ -219,4 +219,22 @@ internal object OnrampDomainModule {
): GetOnrampRedirectUrlUseCase {
return GetOnrampRedirectUrlUseCase(onrampRepository, transactionRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideFetchOnrampCurrenciesUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): FetchOnrampCurrenciesUseCase {
return FetchOnrampCurrenciesUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideFetchOnrampCountriesUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): FetchOnrampCountriesUseCase {
return FetchOnrampCountriesUseCase(onrampRepository, onrampErrorResolver)
}
}

View file

@ -239,6 +239,7 @@ object OnboardingHelper {
scope.launch {
val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
val onrampFeatureToggles = store.inject(DaggerGraphState::onrampFeatureToggles)
val isRussia = if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
@ -248,7 +249,7 @@ object OnboardingHelper {
globalState.userCountryCode == RUSSIA_COUNTRY_CODE
}
if (isRussia) {
if (isRussia && !onrampFeatureToggles.isFeatureEnabled) {
val dialogData = AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(dialogData))
} else {

View file

@ -95,7 +95,7 @@ object TradeCryptoMiddleware {
scope.launch {
val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
val onrampFeatureToggles = store.inject(DaggerGraphState::onrampFeatureToggles)
val isRussia = if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
@ -104,7 +104,7 @@ object TradeCryptoMiddleware {
state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE
}
if (action.checkUserLocation && isRussia) {
if (action.checkUserLocation && isRussia && !onrampFeatureToggles.isFeatureEnabled) {
val dialogData = topUrl?.let {
AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl = it)
}

View file

@ -227,6 +227,15 @@ sealed class NotificationUM(val config: NotificationConfig) {
wrappedList(cryptoAmount, fiatAmount),
),
)
data class OnrampErrorNotification(val onRefresh: () -> Unit) : Warning(
title = resourceReference(R.string.common_error),
subtitle = resourceReference(R.string.common_unknown_error),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.warning_button_refresh),
onClick = onRefresh,
),
)
}
open class Info(

View file

@ -1,6 +1,10 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.onramp.countries.DefaultOnrampCountriesStore
import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore
import com.tangem.datasource.local.onramp.currencies.DefaultOnrampCurrenciesStore
import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore
import com.tangem.datasource.local.onramp.pairs.DefaultOnrampPairsStore
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.DefaultOnrampPaymentMethodsStore
@ -34,4 +38,16 @@ internal object OnrampStoreModule {
fun provideOnrampQuotesStore(): OnrampQuotesStore {
return DefaultOnrampQuotesStore(dataStore = RuntimeDataStore())
}
@Provides
@Singleton
fun provideOnrampCountriesStore(): OnrampCountriesStore {
return DefaultOnrampCountriesStore(dataStore = RuntimeDataStore())
}
@Provides
@Singleton
fun provideOnrampCurrencies(): OnrampCurrenciesStore {
return DefaultOnrampCurrenciesStore(dataStore = RuntimeDataStore())
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.local.onramp.countries
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
import com.tangem.domain.onramp.model.OnrampCountry
internal class DefaultOnrampCountriesStore(
dataStore: StringKeyDataStore<List<OnrampCountry>>,
) : OnrampCountriesStore, StringKeyDataStoreDecorator<String, List<OnrampCountry>>(dataStore) {
override fun provideStringKey(key: String): String = key
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.local.onramp.countries
import com.tangem.domain.onramp.model.OnrampCountry
import kotlinx.coroutines.flow.Flow
interface OnrampCountriesStore {
suspend fun getSyncOrNull(key: String): List<OnrampCountry>?
fun get(key: String): Flow<List<OnrampCountry>>
suspend fun store(key: String, value: List<OnrampCountry>)
suspend fun clear()
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.local.onramp.currencies
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
import com.tangem.domain.onramp.model.OnrampCurrency
internal class DefaultOnrampCurrenciesStore(
dataStore: StringKeyDataStore<List<OnrampCurrency>>,
) : OnrampCurrenciesStore, StringKeyDataStoreDecorator<String, List<OnrampCurrency>>(dataStore) {
override fun provideStringKey(key: String): String = key
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.local.onramp.currencies
import com.tangem.domain.onramp.model.OnrampCurrency
import kotlinx.coroutines.flow.Flow
interface OnrampCurrenciesStore {
suspend fun getSyncOrNull(key: String): List<OnrampCurrency>?
fun get(key: String): Flow<List<OnrampCurrency>>
suspend fun store(key: String, value: List<OnrampCurrency>)
suspend fun clear()
}

View file

@ -3,6 +3,7 @@ package com.tangem.data.onramp
import com.tangem.data.onramp.converters.error.OnrampErrorConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.model.error.OnrampPairsError
import com.tangem.domain.onramp.model.error.OnrampRedirectError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
@ -17,6 +18,7 @@ internal class DefaultOnrampErrorResolver(
}
is OnrampRedirectError.WrongRequestId -> OnrampError.RedirectError.WrongRequestId
is OnrampRedirectError.VerificationFailed -> OnrampError.RedirectError.VerificationFailed
is OnrampPairsError.PairsNotFound -> OnrampError.PairsNotFound
else -> {
OnrampError.DomainError(throwable.message)
}

View file

@ -25,6 +25,8 @@ import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore
import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
@ -38,6 +40,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.onramp.model.*
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.model.error.OnrampPairsError
import com.tangem.domain.onramp.model.error.OnrampRedirectError
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.tokens.model.Amount
@ -65,6 +68,8 @@ internal class DefaultOnrampRepository(
private val paymentMethodsStore: OnrampPaymentMethodsStore,
private val pairsStore: OnrampPairsStore,
private val quotesStore: OnrampQuotesStore,
private val countriesStore: OnrampCountriesStore,
private val currenciesStore: OnrampCurrenciesStore,
private val walletManagersFacade: WalletManagersFacade,
private val dataSignatureVerifier: DataSignatureVerifier,
moshi: Moshi,
@ -78,16 +83,36 @@ internal class DefaultOnrampRepository(
private val onrampErrorAdapter = moshi.adapter(ExpressErrorResponse::class.java)
private val onrampErrorConverter = OnrampErrorConverter(onrampErrorAdapter)
override suspend fun getCurrencies(): List<OnrampCurrency> = withContext(dispatchers.io) {
onrampApi.getCurrencies()
.getOrThrow()
.map(currencyConverter::convert)
override suspend fun getCurrencies(): Flow<List<OnrampCurrency>> = withContext(dispatchers.io) {
currenciesStore.get(CURRENCIES_KEY)
}
override suspend fun getCountries(): List<OnrampCountry> = withContext(dispatchers.io) {
onrampApi.getCountries()
override suspend fun fetchCurrencies() = withContext(dispatchers.io) {
if (!currenciesStore.getSyncOrNull(CURRENCIES_KEY).isNullOrEmpty()) return@withContext
val result = onrampApi.getCurrencies()
.getOrThrow()
.map(currencyConverter::convert)
currenciesStore.store(CURRENCIES_KEY, result)
}
override suspend fun getCountries(): Flow<List<OnrampCountry>> = withContext(dispatchers.io) {
countriesStore.get(COUNTRIES_KEY)
}
override suspend fun getCountriesSync(): List<OnrampCountry>? {
return countriesStore.getSyncOrNull(COUNTRIES_KEY)
}
override suspend fun fetchCountries() = withContext(dispatchers.io) {
if (!countriesStore.getSyncOrNull(COUNTRIES_KEY).isNullOrEmpty()) return@withContext
val result = onrampApi.getCountries()
.getOrThrow()
.map(countryConverter::convert)
countriesStore.store(COUNTRIES_KEY, result)
}
override suspend fun getCountryByIp(): OnrampCountry = withContext(dispatchers.io) {
@ -338,9 +363,12 @@ internal class DefaultOnrampRepository(
paymentMethodsStore.clear()
pairsStore.clear()
quotesStore.clear()
countriesStore.clear()
currenciesStore.clear()
}
private suspend fun storeOnrampPairs(pairs: List<OnrampPairDTO>, providers: List<ExchangeProvider>) {
if (pairs.isEmpty() || providers.isEmpty()) throw OnrampPairsError.PairsNotFound
val onrampPaymentMethods = getPaymentMethods()
val onrampPairs = pairs.map { pair ->
val onrampProviders = pair.providers.mapNotNull { onrampProviderDTO ->
@ -453,6 +481,8 @@ internal class DefaultOnrampRepository(
const val SELECTED_PAYMENT_METHOD_KEY = "onramp_selected_payment_method"
const val PAIRS_KEY = "onramp_pairs"
const val QUOTES_KEY = "onramp_quotes"
const val COUNTRIES_KEY = "onramp_countries"
const val CURRENCIES_KEY = "onramp_currencies"
const val PROVIDER_THEME_DARK = "dark"
const val PROVIDER_THEME_LIGHT = "light"
}

View file

@ -10,6 +10,8 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore
import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
@ -39,6 +41,8 @@ internal object OnrampDataModule {
paymentMethodsStore: OnrampPaymentMethodsStore,
pairsStore: OnrampPairsStore,
quotesStore: OnrampQuotesStore,
countriesStore: OnrampCountriesStore,
currenciesStore: OnrampCurrenciesStore,
walletManagersFacade: WalletManagersFacade,
dataSignatureVerifier: DataSignatureVerifier,
@NetworkMoshi moshi: Moshi,
@ -51,6 +55,8 @@ internal object OnrampDataModule {
paymentMethodsStore = paymentMethodsStore,
pairsStore = pairsStore,
quotesStore = quotesStore,
currenciesStore = currenciesStore,
countriesStore = countriesStore,
walletManagersFacade = walletManagersFacade,
dataSignatureVerifier = dataSignatureVerifier,
moshi = moshi,

View file

@ -29,4 +29,6 @@ sealed class OnrampError {
data class DomainError(
val description: String?,
) : OnrampError()
data object PairsNotFound : OnrampError()
}

View file

@ -0,0 +1,5 @@
package com.tangem.domain.onramp.model.error
sealed class OnrampPairsError : Throwable() {
data object PairsNotFound : OnrampPairsError()
}

View file

@ -26,7 +26,7 @@ class CheckOnrampAvailabilityUseCase(
}
private suspend fun proceedWithSavedCountry(savedCountry: OnrampCountry): OnrampAvailability {
val countries = repository.getCountries()
val countries = repository.getCountriesSync().orEmpty()
val onrampAvailable = countries.find { it == savedCountry }?.onrampAvailable ?: false
return if (onrampAvailable) {
val currency = repository.getDefaultCurrencySync() ?: run {

View file

@ -0,0 +1,16 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
class FetchOnrampCountriesUseCase(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(): Either<OnrampError, Unit> {
return Either.catch { repository.fetchCountries() }.mapLeft(errorResolver::resolve)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
class FetchOnrampCurrenciesUseCase(
private val repository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(): Either<OnrampError, Unit> {
return Either.catch { repository.fetchCurrencies() }.mapLeft(errorResolver::resolve)
}
}

View file

@ -1,17 +1,21 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import kotlinx.coroutines.flow.map
class GetOnrampCountriesUseCase(
private val onrampRepository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(): Either<OnrampError, List<OnrampCountry>> {
return Either.catch { onrampRepository.getCountries() }.mapLeft(errorResolver::resolve)
suspend operator fun invoke(): EitherFlow<OnrampError, List<OnrampCountry>> {
return onrampRepository.getCountries().map {
Either.catch { it }.mapLeft(errorResolver::resolve)
}
}
}

View file

@ -1,23 +1,26 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.onramp.model.OnrampCurrencies
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import kotlinx.coroutines.flow.map
class GetOnrampCurrenciesUseCase(
private val onrampRepository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(): Either<OnrampError, OnrampCurrencies> {
return Either.catch {
val currenciesList = onrampRepository.getCurrencies()
val (populars, others) = currenciesList.toSet()
.partition { popularFiatCodes.contains(it.code.uppercase()) }
OnrampCurrencies(populars = populars, others = others)
}.mapLeft(errorResolver::resolve)
suspend operator fun invoke(): EitherFlow<OnrampError, OnrampCurrencies> {
return onrampRepository.getCurrencies().map { currenciesList ->
Either.catch {
val (populars, others) = currenciesList.toSet()
.partition { popularFiatCodes.contains(it.code.uppercase()) }
OnrampCurrencies(populars = populars, others = others)
}.mapLeft(errorResolver::resolve)
}
}
private companion object {

View file

@ -10,10 +10,13 @@ import kotlinx.coroutines.flow.Flow
@Suppress("TooManyFunctions")
interface OnrampRepository {
// api
suspend fun getCurrencies(): List<OnrampCurrency>
suspend fun getCountries(): List<OnrampCountry>
suspend fun getCurrencies(): Flow<List<OnrampCurrency>>
suspend fun getCountries(): Flow<List<OnrampCountry>>
suspend fun getCountriesSync(): List<OnrampCountry>?
suspend fun getCountryByIp(): OnrampCountry
suspend fun getStatus(txId: String): OnrampStatus
suspend fun fetchCurrencies()
suspend fun fetchCountries()
suspend fun fetchPaymentMethodsIfAbsent()
suspend fun fetchPairs(currency: OnrampCurrency, country: OnrampCountry, cryptoCurrency: CryptoCurrency)
suspend fun fetchQuotes(cryptoCurrency: CryptoCurrency, amount: Amount)

View file

@ -1,5 +1,7 @@
package com.tangem.features.managetokens.utils.list
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastMap
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
@ -165,7 +167,7 @@ internal class ManageTokensListManager @Inject constructor(
}
state.update { state ->
val newBatches = batchListState.data
val newBatches = distinctCurrencies(batchListState.data)
val currentBatches = state.currencyBatches
// Distinct until changed
@ -186,6 +188,28 @@ internal class ManageTokensListManager @Inject constructor(
}
}
// FIXME: Add interception functionality to BatchFlow state and do this on domain
// [REDACTED_JIRA]
private fun distinctCurrencies(
batches: List<Batch<Int, List<ManagedCryptoCurrency>>>,
): List<Batch<Int, List<ManagedCryptoCurrency>>> {
val allCurrenciesIds = mutableListOf<ManagedCryptoCurrency.ID>()
return batches.fastMap { batch ->
val batchCurrencies = batch.data.toMutableList()
batch.data.fastForEachIndexed { index, currency ->
if (currency.id in allCurrenciesIds) {
batchCurrencies.removeAt(index)
} else {
allCurrenciesIds.add(currency.id)
}
}
batch.copy(data = batchCurrencies)
}
}
override fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) {
changedCurrenciesManager.addCurrency(currency, network)

View file

@ -8,4 +8,5 @@ interface OnrampIntents {
fun openCurrenciesList()
fun onBuyClick(quote: OnrampProviderWithQuote.Data)
fun openProviders()
fun onRefresh()
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.onramp.main.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
@ -13,11 +14,13 @@ internal sealed interface OnrampMainComponentUM {
val topBarConfig: OnrampMainTopBarUM
val buyButtonConfig: BuyButtonConfig
val errorNotification: NotificationUM?
data class InitialLoading(
val currency: String,
val onClose: () -> Unit,
val openSettings: () -> Unit,
override val errorNotification: NotificationUM? = null,
) : OnrampMainComponentUM {
override val topBarConfig: OnrampMainTopBarUM = OnrampMainTopBarUM(
title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")),
@ -43,6 +46,7 @@ internal sealed interface OnrampMainComponentUM {
data class Content(
override val topBarConfig: OnrampMainTopBarUM,
override val buyButtonConfig: BuyButtonConfig,
override val errorNotification: NotificationUM?,
val amountBlockState: OnrampAmountBlockUM,
val providerBlockState: OnrampProviderBlockUM,
) : OnrampMainComponentUM

View file

@ -5,12 +5,16 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.convertToAmount
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.*
import com.tangem.utils.Provider
import java.math.BigDecimal
@ -38,9 +42,59 @@ internal class OnrampStateFactory(
buyButtonConfig = state.buyButtonConfig,
amountBlockState = getInitialAmountBlockState(currency),
providerBlockState = OnrampProviderBlockUM.Empty,
errorNotification = null,
)
}
fun getOnrampErrorState(onrampError: OnrampError): OnrampMainComponentUM {
return when (onrampError) {
OnrampError.PairsNotFound -> getNoPairsErrorState()
is OnrampError.DataError,
is OnrampError.DomainError,
-> getErrorState()
is OnrampError.AmountError.TooBigError,
is OnrampError.AmountError.TooSmallError,
OnrampError.RedirectError.VerificationFailed,
OnrampError.RedirectError.WrongRequestId,
-> currentStateProvider() // ignore error state
}
}
private fun getNoPairsErrorState(): OnrampMainComponentUM {
val state = currentStateProvider()
val contentState = state as? OnrampMainComponentUM.Content ?: return state
return contentState.copy(
buyButtonConfig = contentState.buyButtonConfig.copy(enabled = false),
amountBlockState = contentState.amountBlockState.copy(
amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true),
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Error(
error = resourceReference(R.string.onramp_no_available_providers),
),
),
)
}
private fun getErrorState(): OnrampMainComponentUM {
val state = currentStateProvider()
val endButton = state.topBarConfig.endButtonUM.copy(enabled = true)
return when (state) {
is OnrampMainComponentUM.Content -> state.copy(
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
buyButtonConfig = state.buyButtonConfig.copy(enabled = false),
amountBlockState = state.amountBlockState.copy(
amountFieldModel = state.amountBlockState.amountFieldModel.copy(isError = true),
),
providerBlockState = OnrampProviderBlockUM.Empty,
errorNotification = NotificationUM.Warning.OnrampErrorNotification(onrampIntents::onRefresh),
)
is OnrampMainComponentUM.InitialLoading -> state.copy(
errorNotification = NotificationUM.Warning.OnrampErrorNotification(onrampIntents::onRefresh),
)
}
}
private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampAmountBlockUM {
return OnrampAmountBlockUM(
currencyUM = OnrampCurrencyUM(

View file

@ -116,10 +116,7 @@ internal class OnrampMainComponentModel @Inject constructor(
modelScope.launch {
checkOnrampAvailabilityUseCase()
.onRight(::handleOnrampAvailability)
.onLeft { error ->
Timber.e(error.toString())
sendOnrampErrorAnalytic(error)
}
.onLeft(::handleOnrampError)
}
}
@ -136,7 +133,7 @@ internal class OnrampMainComponentModel @Inject constructor(
getOnrampCurrencyUseCase.invoke()
.onEach { maybeCurrency ->
maybeCurrency.fold(
ifLeft = ::sendOnrampErrorAnalytic,
ifLeft = ::handleOnrampError,
ifRight = { currency ->
if (currency == null) return@onEach
_state.update { amountStateFactory.getUpdatedCurrencyState(currency) }
@ -157,10 +154,16 @@ internal class OnrampMainComponentModel @Inject constructor(
}
private suspend fun updatePairsAndQuotes() {
fetchPairsUseCase.invoke(params.cryptoCurrency).onLeft(::sendOnrampErrorAnalytic)
fetchPairsUseCase.invoke(params.cryptoCurrency).onLeft(::handleOnrampError)
startLoadingQuotes()
}
private fun handleOnrampError(onrampError: OnrampError) {
Timber.e(onrampError.toString())
sendOnrampErrorAnalytic(onrampError)
_state.update { stateFactory.getOnrampErrorState(onrampError) }
}
private fun startLoadingQuotes() {
quotesTaskScheduler.cancelTask()
quotesTaskScheduler.scheduleTask(scope = modelScope, task = loadQuotesTask())
@ -176,7 +179,7 @@ internal class OnrampMainComponentModel @Inject constructor(
fetchQuotesUseCase.invoke(
amount = content.amountBlockState.amountFieldModel.fiatAmount,
cryptoCurrency = params.cryptoCurrency,
).onLeft(::sendOnrampErrorAnalytic)
).onLeft(::handleOnrampError)
}
},
onSuccess = {},
@ -188,7 +191,7 @@ internal class OnrampMainComponentModel @Inject constructor(
getOnrampQuotesUseCase.invoke()
.onEach { maybeQuotes ->
maybeQuotes.fold(
ifLeft = ::sendOnrampErrorAnalytic,
ifLeft = ::handleOnrampError,
ifRight = { quotes ->
quotes.filterIsInstance<OnrampQuote.Error>().forEach { errorState ->
sendOnrampErrorAnalytic(errorState.error)
@ -253,6 +256,20 @@ internal class OnrampMainComponentModel @Inject constructor(
)
}
override fun onRefresh() {
_state.update {
stateFactory.getInitialState(
currency = params.cryptoCurrency.name,
onClose = router::pop,
)
}
quotesTaskScheduler.cancelTask()
modelScope.launch {
clearOnrampCacheUseCase.invoke()
checkResidenceCountry()
}
}
override fun onDestroy() {
modelScope.launch { clearOnrampCacheUseCase.invoke() }
quotesTaskScheduler.cancelTask()

View file

@ -18,6 +18,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.core.ui.components.TextShimmer
@ -30,7 +31,6 @@ import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.OnrampAmountBlockUM
import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM
import com.tangem.features.onramp.main.entity.OnrampCurrencyUM
import kotlinx.coroutines.delay
@Composable
internal fun OnrampAmountContent(state: OnrampAmountBlockUM, modifier: Modifier = Modifier) {
@ -41,10 +41,7 @@ internal fun OnrampAmountContent(state: OnrampAmountBlockUM, modifier: Modifier
.padding(vertical = TangemTheme.dimens.spacing28),
horizontalAlignment = Alignment.CenterHorizontally,
) {
OnrampCurrencyIcon(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing24),
currencyUM = state.currencyUM,
)
OnrampCurrencyIcon(currencyUM = state.currencyUM)
OnrampAmountField(amountField = state.amountFieldModel)
OnrampAmountSecondary(state = state.secondaryFieldModel)
}
@ -85,7 +82,6 @@ private fun OnrampAmountField(amountField: AmountFieldModel) {
)
LaunchedEffect(key1 = Unit) {
delay(timeMillis = 200)
requester.requestFocus()
}
}
@ -126,7 +122,10 @@ private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) {
@Composable
private fun OnrampCurrencyIcon(currencyUM: OnrampCurrencyUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier.clickable(onClick = currencyUM.onClick),
modifier = modifier
.clip(RoundedCornerShape(8.dp))
.clickable(onClick = currencyUM.onClick)
.padding(start = TangemTheme.dimens.spacing24),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {

View file

@ -16,6 +16,7 @@ import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
@ -43,16 +44,17 @@ internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier:
.fillMaxWidth()
.wrapContentHeight()
when (state) {
is OnrampMainComponentUM.InitialLoading -> InitialLoading(modifier = contentModifier)
is OnrampMainComponentUM.InitialLoading -> InitialLoading(modifier = contentModifier, state = state)
is OnrampMainComponentUM.Content -> Content(modifier = contentModifier, state = state)
}
},
floatingActionButton = {
BuyButton(
PrimaryButton(
modifier = Modifier
.navigationBarsPadding()
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(id = R.string.common_buy),
onClick = state.buyButtonConfig.onClick,
enabled = state.buyButtonConfig.enabled,
)
@ -62,9 +64,21 @@ internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier:
}
@Composable
private fun InitialLoading(modifier: Modifier = Modifier) {
private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
OnrampAmountContentLoading()
if (state.errorNotification != null) Notification(config = state.errorNotification.config)
}
}
@Composable
private fun OnrampAmountContentLoading(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
.background(TangemTheme.colors.background.action)
.padding(vertical = TangemTheme.dimens.spacing28),
@ -97,15 +111,6 @@ private fun Content(state: OnrampMainComponentUM.Content, modifier: Modifier = M
) {
OnrampAmountContent(state = state.amountBlockState)
OnrampProviderContent(state = state.providerBlockState, modifier = Modifier.fillMaxWidth())
if (state.errorNotification != null) Notification(config = state.errorNotification.config)
}
}
@Composable
private fun BuyButton(onClick: () -> Unit, enabled: Boolean, modifier: Modifier = Modifier) {
PrimaryButton(
modifier = modifier,
text = stringResource(id = R.string.common_buy),
onClick = onClick,
enabled = enabled,
)
}

View file

@ -6,6 +6,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.onramp.FetchOnrampCountriesUseCase
import com.tangem.domain.onramp.GetOnrampCountriesUseCase
import com.tangem.domain.onramp.GetOnrampCountryUseCase
import com.tangem.domain.onramp.OnrampSaveDefaultCountryUseCase
@ -39,6 +40,7 @@ internal class OnrampSelectCountryModel @Inject constructor(
private val getOnrampCountriesUseCase: GetOnrampCountriesUseCase,
private val saveDefaultCountryUseCase: OnrampSaveDefaultCountryUseCase,
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,
private val fetchOnrampCountriesUseCase: FetchOnrampCountriesUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@ -52,9 +54,16 @@ internal class OnrampSelectCountryModel @Inject constructor(
init {
analyticsEventHandler.send(OnrampAnalyticsEvent.SelectResidenceOpened)
updateCountriesList()
modelScope.launch { subscribeOnUpdateState() }
}
private fun updateCountriesList() {
modelScope.launch {
fetchOnrampCountriesUseCase()
}
}
fun dismiss() {
params.onDismiss()
}
@ -62,7 +71,7 @@ internal class OnrampSelectCountryModel @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun subscribeOnUpdateState() {
combine(
flow = refreshTrigger.onStart { emit(Unit) }.flatMapLatest { flowOf(getOnrampCountriesUseCase()) },
flow = refreshTrigger.onStart { emit(Unit) }.flatMapLatest { getOnrampCountriesUseCase() },
flow2 = getOnrampCountryUseCase(),
flow3 = searchManager.query,
) { maybeCountries, maybeCountry, query ->

View file

@ -7,6 +7,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.onramp.FetchOnrampCurrenciesUseCase
import com.tangem.domain.onramp.GetOnrampCurrenciesUseCase
import com.tangem.domain.onramp.OnrampSaveDefaultCurrencyUseCase
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
@ -31,6 +32,7 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@ComponentScoped
internal class OnrampSelectCurrencyModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
@ -38,6 +40,7 @@ internal class OnrampSelectCurrencyModel @Inject constructor(
private val searchManager: InputManager,
private val getOnrampCurrenciesUseCase: GetOnrampCurrenciesUseCase,
private val saveDefaultCurrencyUseCase: OnrampSaveDefaultCurrencyUseCase,
private val fetchOnrampCurrenciesUseCase: FetchOnrampCurrenciesUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@ -51,13 +54,20 @@ internal class OnrampSelectCurrencyModel @Inject constructor(
private val refreshTrigger = MutableSharedFlow<Unit>()
init {
updateCurrenciesList()
subscribeOnUpdateState()
}
private fun updateCurrenciesList() {
modelScope.launch {
fetchOnrampCurrenciesUseCase()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun subscribeOnUpdateState() {
combine(
flow = refreshTrigger.onStart { emit(Unit) }.flatMapLatest { flowOf(getOnrampCurrenciesUseCase.invoke()) },
flow = refreshTrigger.onStart { emit(Unit) }.flatMapLatest { getOnrampCurrenciesUseCase() },
flow2 = searchManager.query,
) { maybeCurrencies, query ->
maybeCurrencies.onLeft {

View file

@ -28,5 +28,6 @@ internal fun AnalyticsEventHandler.sendOnrampErrorEvent(
is OnrampError.AmountError.TooSmallError,
OnrampError.RedirectError.VerificationFailed,
OnrampError.RedirectError.WrongRequestId,
OnrampError.PairsNotFound,
-> { /* no-op */ }
}

View file

@ -49,6 +49,12 @@ internal enum class Wallet2CobrandImage(
batchIds = setOf("AF33"),
),
BTC365(
cards2ResId = R.drawable.ill_btc365_card2_120_106,
cards3ResId = R.drawable.ill_btc365_card3_120_106,
batchIds = setOf("AF97"),
),
CoinMetrica(
cards2ResId = R.drawable.ill_coin_metrica_card2_120_106,
cards3ResId = R.drawable.ill_coin_metrica_card3_120_106,
@ -115,12 +121,36 @@ internal enum class Wallet2CobrandImage(
batchIds = setOf("AF73"),
),
Kasper(
cards2ResId = R.drawable.ill_kasper_card2_120_106,
cards3ResId = R.drawable.ill_kasper_card3_120_106,
batchIds = setOf("AF96"),
),
Kaspy(
cards2ResId = R.drawable.ill_kaspy_card2_120_106,
cards3ResId = R.drawable.ill_kaspy_card3_120_106,
batchIds = setOf("AF95"),
),
KishuInu(
cards2ResId = R.drawable.ill_kishu_inu_card2_120_106,
cards3ResId = R.drawable.ill_kishu_inu_card3_120_106,
batchIds = setOf("AF52"),
),
Konan(
cards2ResId = R.drawable.ill_konan_card2_120_106,
cards3ResId = R.drawable.ill_konan_card3_120_106,
batchIds = setOf("AF93"),
),
Neiro(
cards2ResId = R.drawable.ill_neiro_card2_120_106,
cards3ResId = R.drawable.ill_neiro_card3_120_106,
batchIds = setOf("AF98"),
),
NewWorldElite(
cards2ResId = R.drawable.ill_nwe_card2_120_106,
cards3ResId = R.drawable.ill_nwe_card3_120_106,

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB