Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-09 17:44:25 +05:00
parent e0594fa9f8
commit 462dd631b7
17 changed files with 199 additions and 25 deletions

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

@ -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

@ -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
@ -66,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,
@ -79,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) {
@ -339,6 +363,8 @@ internal class DefaultOnrampRepository(
paymentMethodsStore.clear()
pairsStore.clear()
quotesStore.clear()
countriesStore.clear()
currenciesStore.clear()
}
private suspend fun storeOnrampPairs(pairs: List<OnrampPairDTO>, providers: List<ExchangeProvider>) {
@ -455,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

@ -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

@ -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
@ -40,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)
}
@ -124,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

@ -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 {