Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-23 13:36:37 +03:00
parent bc8a0ba20d
commit a733dd0e52
13 changed files with 158 additions and 25 deletions

View file

@ -11,6 +11,10 @@ import com.tangem.datasource.local.onramp.paymentmethods.DefaultOnrampPaymentMet
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.DefaultOnrampQuotesStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
import com.tangem.datasource.local.onramp.sepa.DefaultOnrampCurrentCountryByIPStore
import com.tangem.datasource.local.onramp.sepa.DefaultOnrampSepaAvailabilityStore
import com.tangem.datasource.local.onramp.sepa.OnrampCurrentCountryByIPStore
import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -50,4 +54,16 @@ internal object OnrampStoreModule {
fun provideOnrampCurrencies(): OnrampCurrenciesStore {
return DefaultOnrampCurrenciesStore(dataStore = RuntimeDataStore())
}
@Provides
@Singleton
fun provideOnrampSepaAvailableStore(): OnrampSepaAvailabilityStore {
return DefaultOnrampSepaAvailabilityStore(dataStore = RuntimeDataStore())
}
@Provides
@Singleton
fun provideOnrampCurrentCountryByIPStore(): OnrampCurrentCountryByIPStore {
return DefaultOnrampCurrentCountryByIPStore(dataStore = RuntimeDataStore())
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.local.onramp.sepa
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 DefaultOnrampCurrentCountryByIPStore(
val dataStore: StringKeyDataStore<OnrampCountry>,
) : OnrampCurrentCountryByIPStore, StringKeyDataStoreDecorator<Unit, OnrampCountry>(
wrappedDataStore = dataStore,
) {
override suspend fun getSyncOrNull(): OnrampCountry? {
return getSyncOrNull(Unit)
}
override suspend fun store(value: OnrampCountry) {
return store(Unit, value)
}
override fun provideStringKey(key: Unit) = "KEY"
}

View file

@ -0,0 +1,20 @@
package com.tangem.datasource.local.onramp.sepa
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
internal class DefaultOnrampSepaAvailabilityStore(
val dataStore: StringKeyDataStore<Boolean>,
) : OnrampSepaAvailabilityStore, StringKeyDataStoreDecorator<OnrampSepaAvailabilityStoreKey, Boolean>(
wrappedDataStore = dataStore,
) {
override fun provideStringKey(key: OnrampSepaAvailabilityStoreKey) = with(key) {
buildString {
append(userWallet.walletId.toString())
append("_")
append(country.code)
append("_")
append(cryptoCurrency.id.value)
}
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.local.onramp.sepa
import com.tangem.domain.onramp.model.OnrampCountry
interface OnrampCurrentCountryByIPStore {
suspend fun getSyncOrNull(): OnrampCountry?
suspend fun store(value: OnrampCountry)
suspend fun clear()
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.local.onramp.sepa
import kotlinx.coroutines.flow.Flow
interface OnrampSepaAvailabilityStore {
suspend fun getSyncOrNull(key: OnrampSepaAvailabilityStoreKey): Boolean?
fun get(key: OnrampSepaAvailabilityStoreKey): Flow<Boolean>
suspend fun store(key: OnrampSepaAvailabilityStoreKey, value: Boolean)
suspend fun clear()
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.local.onramp.sepa
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.model.OnrampCountry
data class OnrampSepaAvailabilityStoreKey(
val userWallet: UserWallet,
val country: OnrampCountry,
val cryptoCurrency: CryptoCurrency,
)

View file

@ -27,10 +27,12 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
@ -216,7 +218,7 @@ internal fun TextsBlock(
SpacerH(height = TangemTheme.dimens.spacing2)
}
val subtitleText = subtitle.resolveReference()
val subtitleText = subtitle.resolveAnnotatedReference()
if (subtitleText.isNotEmpty()) {
Text(
text = subtitleText,
@ -459,5 +461,15 @@ private class NotificationConfigProvider : CollectionPreviewParameterProvider<No
subtitle = resourceReference(id = R.string.information_generated_with_ai),
iconResId = R.drawable.ic_magic_28,
),
NotificationConfig(
title = resourceReference(R.string.notification_sepa_title),
subtitle = resourceReference(R.string.notification_sepa_text),
iconResId = R.drawable.img_notification_sepa,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.notification_sepa_button),
onClick = { },
),
iconSize = 54.dp,
),
),
)

View file

@ -30,6 +30,9 @@ 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
import com.tangem.datasource.local.onramp.sepa.OnrampCurrentCountryByIPStore
import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStore
import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStoreKey
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
@ -64,6 +67,8 @@ internal class DefaultOnrampRepository(
private val dispatchers: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
private val paymentMethodsStore: OnrampPaymentMethodsStore,
private val onrampSepaAvailabilityStore: OnrampSepaAvailabilityStore,
private val onrampCurrentCountryByIPStore: OnrampCurrentCountryByIPStore,
private val pairsStore: OnrampPairsStore,
private val quotesStore: OnrampQuotesStore,
private val countriesStore: OnrampCountriesStore,
@ -123,8 +128,17 @@ internal class DefaultOnrampRepository(
result
}
override suspend fun getCountryByIp(userWallet: UserWallet): OnrampCountry = withContext(dispatchers.io) {
onrampApi.getCountryByIp(
override suspend fun getCountryByIp(userWallet: UserWallet, fromCache: Boolean): OnrampCountry =
withContext(dispatchers.io) {
if (fromCache) {
val country = onrampCurrentCountryByIPStore.getSyncOrNull()
if (country != null) {
return@withContext country
}
}
val country = onrampApi.getCountryByIp(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
@ -133,6 +147,10 @@ internal class DefaultOnrampRepository(
)
.getOrThrow()
.let(countryConverter::convert)
onrampCurrentCountryByIPStore.store(country)
country
}
override suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus = withContext(dispatchers.io) {
@ -263,11 +281,22 @@ internal class DefaultOnrampRepository(
override suspend fun hasSepaMethod(
userWallet: UserWallet,
currency: OnrampCurrency,
country: OnrampCountry,
cryptoCurrency: CryptoCurrency,
): Boolean {
return withContext(dispatchers.io) {
val key = OnrampSepaAvailabilityStoreKey(
userWallet = userWallet,
country = country,
cryptoCurrency = cryptoCurrency,
)
val cachedValue = onrampSepaAvailabilityStore.getSyncOrNull(key)
if (cachedValue != null) {
return@withContext cachedValue
}
val onrampPairs =
safeApiCall(
call = {
@ -300,6 +329,8 @@ internal class DefaultOnrampRepository(
.flatMap { it.paymentMethods }
.any { it == SEPA_METHOD_ID }
onrampSepaAvailabilityStore.store(key, hasSepaMethod)
hasSepaMethod
}
}

View file

@ -22,6 +22,8 @@ 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
import com.tangem.datasource.local.onramp.sepa.OnrampCurrentCountryByIPStore
import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
@ -52,6 +54,8 @@ internal object OnrampDataModule {
currenciesStore: OnrampCurrenciesStore,
walletManagersFacade: WalletManagersFacade,
dataSignatureVerifier: DataSignatureVerifier,
onrampSepaAvailabilityStore: OnrampSepaAvailabilityStore,
onrampCurrentCountryByIPStore: OnrampCurrentCountryByIPStore,
@NetworkMoshi moshi: Moshi,
): OnrampRepository {
return DefaultOnrampRepository(
@ -60,6 +64,8 @@ internal object OnrampDataModule {
dispatchers = dispatchers,
appPreferencesStore = appPreferencesStore,
paymentMethodsStore = paymentMethodsStore,
onrampSepaAvailabilityStore = onrampSepaAvailabilityStore,
onrampCurrentCountryByIPStore = onrampCurrentCountryByIPStore,
pairsStore = pairsStore,
quotesStore = quotesStore,
currenciesStore = currenciesStore,

View file

@ -26,7 +26,12 @@ class GetOnrampCountryUseCase(
}
suspend fun invokeSync(userWallet: UserWallet): Either<OnrampError, OnrampCountry> {
return Either.catch { repository.getDefaultCountrySync() ?: repository.getCountryByIp(userWallet) }
return Either.catch {
repository.getDefaultCountrySync() ?: repository.getCountryByIp(
userWallet = userWallet,
fromCache = true,
)
}
.mapLeft(errorResolver::resolve)
}
}

View file

@ -6,7 +6,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
class OnrampSepaAvailableUseCase(
private val repository: OnrampRepository,
@ -14,7 +13,6 @@ class OnrampSepaAvailableUseCase(
suspend operator fun invoke(
userWallet: UserWallet,
currency: OnrampCurrency,
country: OnrampCountry,
cryptoCurrency: CryptoCurrency,
): Boolean {
@ -25,7 +23,6 @@ class OnrampSepaAvailableUseCase(
return Either.catch {
repository.hasSepaMethod(
userWallet = userWallet,
currency = currency,
country = country,
cryptoCurrency = cryptoCurrency,
)

View file

@ -13,14 +13,9 @@ interface OnrampRepository {
fun getCurrencies(): Flow<List<OnrampCurrency>>
fun getCountries(): Flow<List<OnrampCountry>>
suspend fun getCountriesSync(): List<OnrampCountry>?
suspend fun getCountryByIp(userWallet: UserWallet): OnrampCountry
suspend fun getCountryByIp(userWallet: UserWallet, fromCache: Boolean = false): OnrampCountry
suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus
suspend fun hasSepaMethod(
userWallet: UserWallet,
currency: OnrampCurrency,
country: OnrampCountry,
cryptoCurrency: CryptoCurrency,
): Boolean
suspend fun hasSepaMethod(userWallet: UserWallet, country: OnrampCountry, cryptoCurrency: CryptoCurrency): Boolean
suspend fun fetchCurrencies(userWallet: UserWallet)
suspend fun fetchCountries(userWallet: UserWallet): List<OnrampCountry>
suspend fun fetchPaymentMethodsIfAbsent(userWallet: UserWallet)

View file

@ -254,7 +254,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val isSepaAvailable = onrampSepaAvailableUseCase(
userWallet = userWallet,
country = country,
currency = country.defaultCurrency,
cryptoCurrency = bitcoinCurrency,
)