Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-20 14:34:19 +05:00
parent 851d9ffea9
commit 9f65e1b80c
18 changed files with 347 additions and 2 deletions

View file

@ -96,4 +96,10 @@ internal object OnrampDomainModule {
): OnrampSaveTransactionUseCase {
return OnrampSaveTransactionUseCase(onrampTransactionRepository)
}
@Provides
@Singleton
fun provideGetOnrampPaymentMethodsUseCase(onrampRepository: OnrampRepository): GetOnrampPaymentMethodsUseCase {
return GetOnrampPaymentMethodsUseCase(onrampRepository)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.onramp.DefaultOnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.OnrampPaymentMethodsStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object OnrampStoreModule {
@Provides
@Singleton
fun provideOnrampPaymentMethodsStore(): OnrampPaymentMethodsStore {
return DefaultOnrampPaymentMethodsStore(dataStore = RuntimeDataStore())
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.local.onramp
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
internal class DefaultOnrampPaymentMethodsStore(
dataStore: StringKeyDataStore<List<PaymentMethodDTO>>,
) : OnrampPaymentMethodsStore, StringKeyDataStoreDecorator<String, List<PaymentMethodDTO>>(dataStore) {
override fun provideStringKey(key: String): String = key
}

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.local.onramp
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
interface OnrampPaymentMethodsStore {
suspend fun getSyncOrNull(key: String): List<PaymentMethodDTO>?
suspend fun store(key: String, value: List<PaymentMethodDTO>)
suspend fun contains(key: String): Boolean
}

View file

@ -1,12 +1,15 @@
package com.tangem.data.onramp
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.onramp.converters.CountryConverter
import com.tangem.data.onramp.converters.CurrencyConverter
import com.tangem.data.onramp.converters.StatusConverter
import com.tangem.data.onramp.converters.PaymentMethodConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.datasource.local.onramp.OnrampPaymentMethodsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
@ -15,21 +18,25 @@ import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import timber.log.Timber
internal class DefaultOnrampRepository(
private val onrampApi: OnrampApi,
private val dispatchers: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
private val paymentMethodsStore: OnrampPaymentMethodsStore,
) : OnrampRepository {
private val currencyConverter = CurrencyConverter()
private val countryConverter = CountryConverter(currencyConverter)
private val statusConverter = StatusConverter()
private val paymentMethodsConverter = PaymentMethodConverter()
override suspend fun getCurrencies(): List<OnrampCurrency> = withContext(dispatchers.io) {
onrampApi.getCurrencies()
@ -92,4 +99,28 @@ internal class DefaultOnrampRepository(
.getObject<OnrampCountryDTO>(PreferencesKeys.ONRAMP_DEFAULT_COUNTRY)
.map { it?.let(countryConverter::convert) }
}
override suspend fun fetchPaymentMethodsIfAbsent() {
if (paymentMethodsStore.contains(PAYMENT_METHODS_KEY)) return
val response = safeApiCall(
call = { onrampApi.getPaymentMethods().bind() },
onError = {
Timber.w(it, "Unable to fetch onramp payment methods")
throw it
},
)
paymentMethodsStore.store(PAYMENT_METHODS_KEY, response)
}
override suspend fun getPaymentMethods(): List<OnrampPaymentMethod> {
val paymentMethods = requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) {
"Onramp payment methods is absent in storage"
}
return paymentMethodsConverter.convertList(paymentMethods)
}
private companion object {
const val PAYMENT_METHODS_KEY = "onramp_payment_methods"
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.data.onramp.converters
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.utils.converter.Converter
internal class PaymentMethodConverter : Converter<PaymentMethodDTO, OnrampPaymentMethod> {
override fun convert(value: PaymentMethodDTO): OnrampPaymentMethod = OnrampPaymentMethod(
id = value.id,
name = value.name,
imageUrl = value.image,
)
}

View file

@ -4,6 +4,7 @@ import com.tangem.data.onramp.DefaultOnrampErrorResolver
import com.tangem.data.onramp.DefaultOnrampRepository
import com.tangem.data.onramp.DefaultOnrampTransactionRepository
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.local.onramp.OnrampPaymentMethodsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
@ -25,11 +26,13 @@ internal object OnrampDataModule {
onrampApi: OnrampApi,
dispatchers: CoroutineDispatcherProvider,
appPreferencesStore: AppPreferencesStore,
paymentMethodsStore: OnrampPaymentMethodsStore,
): OnrampRepository {
return DefaultOnrampRepository(
onrampApi = onrampApi,
dispatchers = dispatchers,
appPreferencesStore = appPreferencesStore,
paymentMethodsStore = paymentMethodsStore,
)
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.onramp.model
import kotlinx.serialization.Serializable
@Serializable
data class OnrampPaymentMethod(
val id: String,
val name: String,
val imageUrl: String,
)

View file

@ -8,6 +8,7 @@ class CheckOnrampAvailabilityUseCase(private val repository: OnrampRepository) {
suspend operator fun invoke(): Either<Throwable, OnrampAvailability> {
return Either.catch {
repository.fetchPaymentMethodsIfAbsent()
repository.getDefaultCountrySync()?.let { savedCountry ->
return@catch if (savedCountry.onrampAvailable) {
val currency = repository.getDefaultCurrencySync() ?: run {

View file

@ -0,0 +1,12 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.repositories.OnrampRepository
class GetOnrampPaymentMethodsUseCase(private val repository: OnrampRepository) {
suspend operator fun invoke(): Either<Throwable, List<OnrampPaymentMethod>> {
return Either.catch { repository.getPaymentMethods() }
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.domain.onramp.repositories
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import kotlinx.coroutines.flow.Flow
interface OnrampRepository {
@ -11,6 +12,7 @@ interface OnrampRepository {
suspend fun getCountries(): List<OnrampCountry>
suspend fun getCountryByIp(): OnrampCountry
suspend fun getStatus(txId: String): OnrampStatus
suspend fun fetchPaymentMethodsIfAbsent()
// cache
suspend fun saveDefaultCurrency(currency: OnrampCurrency)
@ -19,4 +21,5 @@ interface OnrampRepository {
suspend fun saveDefaultCountry(country: OnrampCountry)
suspend fun getDefaultCountrySync(): OnrampCountry?
fun getDefaultCountry(): Flow<OnrampCountry?>
suspend fun getPaymentMethods(): List<OnrampPaymentMethod>
}

View file

@ -8,20 +8,23 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.onramp.GetOnrampCountryUseCase
import com.tangem.domain.onramp.OnrampSaveDefaultCountryUseCase
import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent
import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyBottomSheetConfig
import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyUM
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyBottomSheetConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@ComponentScoped
internal class ConfirmResidencyModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val saveDefaultCountryUseCase: OnrampSaveDefaultCountryUseCase,
getOnrampCountryUseCase: GetOnrampCountryUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@ -54,7 +57,9 @@ internal class ConfirmResidencyModel @Inject constructor(
private fun getPrimaryButtonConfig() = if (params.country.onrampAvailable) {
ConfirmResidencyUM.ActionButtonConfig(
onClick = { params.onDismiss(params.country) },
onClick = {
modelScope.launch { saveDefaultCountryUseCase.invoke(params.country) }
},
text = resourceReference(R.string.common_confirm),
)
} else {

View file

@ -0,0 +1,45 @@
package com.tangem.features.onramp.paymentmethod
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodsBottomSheetConfig
import com.tangem.features.onramp.paymentmethod.ui.SelectPaymentMethodBottomSheet
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.collections.immutable.toImmutableList
internal class DefaultSelectPaymentMethodComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: SelectPaymentMethodComponent.Params,
) : SelectPaymentMethodComponent, AppComponentContext by context {
override fun dismiss() {
params.onDismiss()
}
@Composable
override fun BottomSheet() {
val bottomSheetConfig = remember(key1 = this) {
TangemBottomSheetConfig(
isShow = true,
onDismissRequest = ::dismiss,
content = PaymentMethodsBottomSheetConfig(
selectedMethodId = params.selectedMethodId,
paymentMethods = params.paymentMethods.toImmutableList(),
),
)
}
SelectPaymentMethodBottomSheet(bottomSheetConfig)
}
@AssistedFactory
interface Factory : SelectPaymentMethodComponent.Factory {
override fun create(
context: AppComponentContext,
params: SelectPaymentMethodComponent.Params,
): DefaultSelectPaymentMethodComponent
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.onramp.paymentmethod
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM
internal interface SelectPaymentMethodComponent : ComposableBottomSheetComponent {
data class Params(
val selectedMethodId: String,
val paymentMethods: List<PaymentMethodUM>,
val onDismiss: () -> Unit,
)
interface Factory : ComponentFactory<Params, SelectPaymentMethodComponent>
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.onramp.paymentmethod.di
import com.tangem.features.onramp.paymentmethod.DefaultSelectPaymentMethodComponent
import com.tangem.features.onramp.paymentmethod.SelectPaymentMethodComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface SelectPaymentMethodComponentModule {
@Binds
@Singleton
fun bindSelectPaymentMethodComponentFactory(
factory: DefaultSelectPaymentMethodComponent.Factory,
): SelectPaymentMethodComponent.Factory
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.onramp.paymentmethod.entity
data class PaymentMethodUM(
val id: String,
val imageUrl: String,
val name: String,
val onSelect: () -> Unit,
)

View file

@ -0,0 +1,9 @@
package com.tangem.features.onramp.paymentmethod.entity
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import kotlinx.collections.immutable.ImmutableList
internal data class PaymentMethodsBottomSheetConfig(
val selectedMethodId: String,
val paymentMethods: ImmutableList<PaymentMethodUM>,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,118 @@
package com.tangem.features.onramp.paymentmethod.ui
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import coil.compose.AsyncImage
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM
import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodsBottomSheetConfig
import kotlinx.collections.immutable.ImmutableList
@Composable
internal fun SelectPaymentMethodBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<PaymentMethodsBottomSheetConfig>(
config = config,
addBottomInsets = true,
containerColor = TangemTheme.colors.background.tertiary,
titleText = resourceReference(R.string.onramp_pay_with),
content = { contentConfig ->
SelectPaymentMethodBottomSheetContent(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing24)
.fillMaxWidth()
.wrapContentHeight(),
selectedMethodId = contentConfig.selectedMethodId,
methods = contentConfig.paymentMethods,
)
},
)
}
@Composable
private fun SelectPaymentMethodBottomSheetContent(
selectedMethodId: String,
methods: ImmutableList<PaymentMethodUM>,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier,
) {
items(
items = methods,
key = { item -> item.id },
contentType = { item -> item::class.java },
itemContent = { item ->
val isSelected = remember(item, selectedMethodId) {
item.id == selectedMethodId
}
val itemModifier = if (isSelected) {
Modifier
.fillMaxWidth()
.border(
width = TangemTheme.dimens.size1,
color = TangemTheme.colors.text.accent,
shape = RoundedCornerShape(TangemTheme.dimens.radius16),
)
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
.clickable(onClick = item.onSelect)
.padding(
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,
)
} else {
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
.clickable(onClick = item.onSelect)
.padding(
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,
)
}
PaymentMethodItem(
modifier = itemModifier,
paymentMethod = item,
isSelected = isSelected,
)
},
)
}
}
@Composable
private fun PaymentMethodItem(paymentMethod: PaymentMethodUM, isSelected: Boolean, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
AsyncImage(
modifier = Modifier
.size(TangemTheme.dimens.size40),
model = paymentMethod.imageUrl,
contentDescription = null,
)
Text(
text = paymentMethod.name,
style = TangemTheme.typography.subtitle2,
color = if (isSelected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary,
)
}
}