diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index f15fd1b0c0..81e25125ae 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -47,4 +47,10 @@ internal object OnrampDomainModule { fun provideCheckOnrampAvailabilityUseCase(onrampRepository: OnrampRepository): CheckOnrampAvailabilityUseCase { return CheckOnrampAvailabilityUseCase(onrampRepository) } + + @Provides + @Singleton + fun provideGetOnrampCurrencyUseCase(onrampRepository: OnrampRepository): GetOnrampCurrencyUseCase { + return GetOnrampCurrencyUseCase(onrampRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 8678616017..48042171a4 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -199,7 +199,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.Onramp -> { route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = OnrampComponent.Params(route.currency.name), + params = OnrampComponent.Params(route.currency), componentFactory = onrampComponentFactory, ) } diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAvailability.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAvailability.kt index 8113348d12..601907de1a 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAvailability.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAvailability.kt @@ -2,7 +2,9 @@ package com.tangem.domain.onramp.model sealed interface OnrampAvailability { - data object Available : OnrampAvailability - data class NotSupported(val country: OnrampCountry) : OnrampAvailability - data class ConfirmResidency(val country: OnrampCountry) : OnrampAvailability + val country: OnrampCountry + + data class Available(override val country: OnrampCountry, val currency: OnrampCurrency) : OnrampAvailability + data class NotSupported(override val country: OnrampCountry) : OnrampAvailability + data class ConfirmResidency(override val country: OnrampCountry) : OnrampAvailability } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt index c2aa371b07..46af00376b 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt @@ -8,10 +8,13 @@ class CheckOnrampAvailabilityUseCase(private val repository: OnrampRepository) { suspend operator fun invoke(): Either { return Either.catch { - return@catch OnrampAvailability.Available repository.getDefaultCountrySync()?.let { savedCountry -> return@catch if (savedCountry.onrampAvailable) { - OnrampAvailability.Available + val currency = repository.getDefaultCurrencySync() ?: run { + repository.saveDefaultCurrency(savedCountry.defaultCurrency) + savedCountry.defaultCurrency + } + OnrampAvailability.Available(country = savedCountry, currency = currency) } else { OnrampAvailability.NotSupported(savedCountry) } diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrencyUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrencyUseCase.kt new file mode 100644 index 0000000000..d2fc5147d1 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrencyUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.domain.onramp.repositories.OnrampRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +class GetOnrampCurrencyUseCase(private val repository: OnrampRepository) { + + operator fun invoke(): Flow> { + return repository.getDefaultCurrency() + .map> { it.right() } + .catch { emit(it.left()) } + } +} \ No newline at end of file diff --git a/features/onramp/api/build.gradle.kts b/features/onramp/api/build.gradle.kts index cde16c9b92..632fd29071 100644 --- a/features/onramp/api/build.gradle.kts +++ b/features/onramp/api/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(projects.core.ui) /* Project - Domain */ + implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) /* Compose */ diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt index 219536ddb7..52be1340e5 100644 --- a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt @@ -2,10 +2,11 @@ package com.tangem.features.onramp.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrency interface OnrampComponent : ComposableContentComponent { - data class Params(val currency: String) + data class Params(val cryptoCurrency: CryptoCurrency) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt index 25e5bce171..fe1f307998 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt @@ -17,7 +17,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.onramp.confirmresidency.model.ConfirmResidencyModel import com.tangem.features.onramp.confirmresidency.ui.ConfirmResidencyBottomSheet import com.tangem.features.onramp.confirmresidency.ui.ConfirmResidencyBottomSheetContent -import com.tangem.features.onramp.entity.ConfirmResidencyBottomSheetConfig +import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyBottomSheetConfig import com.tangem.features.onramp.selectcountry.SelectCountryComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/entity/ConfirmResidencyBottomSheetConfig.kt similarity index 77% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyBottomSheetConfig.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/entity/ConfirmResidencyBottomSheetConfig.kt index cccafaaa8d..baacb6b92a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyBottomSheetConfig.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/entity/ConfirmResidencyBottomSheetConfig.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.entity +package com.tangem.features.onramp.confirmresidency.entity import kotlinx.serialization.Serializable diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt index 2b0f91f3d1..25a7058831 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt @@ -11,7 +11,7 @@ import com.tangem.domain.onramp.GetOnrampCountryUseCase import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyUM import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.entity.ConfirmResidencyBottomSheetConfig +import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyBottomSheetConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt index be1d4dcb81..8e4f11228c 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt @@ -16,6 +16,7 @@ import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig import com.tangem.features.onramp.main.model.OnrampMainComponentModel import com.tangem.features.onramp.main.ui.OnrampMainComponentContent +import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -24,6 +25,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: OnrampMainComponent.Params, private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, + private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory, ) : OnrampMainComponent, AppComponentContext by appComponentContext { private val model: OnrampMainComponentModel = getOrCreateModel(params) @@ -54,6 +56,10 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( onDismiss = { model.bottomSheetNavigation.dismiss() }, ), ) + is OnrampMainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create( + context = childByContext(componentContext), + params = SelectCurrencyComponent.Params(model.bottomSheetNavigation::dismiss), + ) } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt index 22592a4268..de585fef2d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt @@ -2,10 +2,11 @@ package com.tangem.features.onramp.main import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrency internal interface OnrampMainComponent : ComposableContentComponent { - class Params(val currency: String, val openSettings: () -> Unit) + data class Params(val cryptoCurrency: CryptoCurrency, val openSettings: () -> Unit) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt new file mode 100644 index 0000000000..d0cfbe8515 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.main.entity + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.core.ui.extensions.TextReference + +internal data class OnrampAmountBlockUM( + val currencyUM: OnrampCurrencyUM, + val amountFieldModel: AmountFieldModel, + val secondaryFieldModel: OnrampAmountSecondaryFieldUM, +) + +internal data class OnrampCurrencyUM(val code: String, val iconUrl: String, val precision: Int, val onClick: () -> Unit) + +@Immutable +internal sealed interface OnrampAmountSecondaryFieldUM { + data object Loading : OnrampAmountSecondaryFieldUM + data class Content(val amount: TextReference) : OnrampAmountSecondaryFieldUM + data class Error(val error: TextReference) : OnrampAmountSecondaryFieldUM +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt new file mode 100644 index 0000000000..0b1002a3bd --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt @@ -0,0 +1,8 @@ +package com.tangem.features.onramp.main.entity + +interface OnrampIntents { + fun onAmountValueChanged(value: String) + fun openSettings() + fun openCurrenciesList() + fun onBuyClick() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt index 03c2357074..397816b229 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt @@ -7,4 +7,7 @@ import kotlinx.serialization.Serializable internal sealed interface OnrampMainBottomSheetConfig { @Serializable data class ConfirmResidency(val country: OnrampCountry) : OnrampMainBottomSheetConfig + + @Serializable + data object CurrenciesList : OnrampMainBottomSheetConfig } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt index 91f5ee356e..b909ee310e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt @@ -44,6 +44,7 @@ internal sealed interface OnrampMainComponentUM { data class Content( override val topBarConfig: OnrampMainTopBarUM, override val buyButtonConfig: BuyButtonConfig, + val amountBlockState: OnrampAmountBlockUM, ) : OnrampMainComponentUM } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt new file mode 100644 index 0000000000..6c40dafb39 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -0,0 +1,85 @@ +package com.tangem.features.onramp.main.entity.factory + +import androidx.compose.foundation.text.KeyboardActions +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.core.ui.extensions.TextReference +import com.tangem.domain.onramp.model.OnrampCurrency +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.main.entity.* +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 com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.utils.Provider +import java.math.BigDecimal + +internal class OnrampStateFactory( + private val currentStateProvider: Provider, + private val cryptoCurrency: CryptoCurrency, + private val onrampIntents: OnrampIntents, +) { + + fun getInitialState(currency: String, onClose: () -> Unit): OnrampMainComponentUM.InitialLoading { + return OnrampMainComponentUM.InitialLoading( + currency = currency, + onClose = onClose, + openSettings = onrampIntents::openSettings, + onBuyClick = onrampIntents::onBuyClick, + ) + } + + fun getReadyState(currency: OnrampCurrency): OnrampMainComponentUM.Content { + val state = currentStateProvider() + + val endButton = state.topBarConfig.endButtonUM.copy(enabled = true) + return OnrampMainComponentUM.Content( + topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), + buyButtonConfig = state.buyButtonConfig, + amountBlockState = getInitialAmountBlockState(currency), + ) + } + + private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampAmountBlockUM { + return OnrampAmountBlockUM( + currencyUM = OnrampCurrencyUM( + code = currency.code, + iconUrl = currency.image, + precision = currency.precision, + onClick = onrampIntents::openCurrenciesList, + ), + amountFieldModel = AmountFieldModel( + value = "", + fiatValue = "", + onValueChange = onrampIntents::onAmountValueChanged, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.None, + keyboardType = KeyboardType.Number, + ), + keyboardActions = KeyboardActions(), + isFiatValue = true, + cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrency), + fiatAmount = BigDecimal.ZERO.convertToFiatAmount(currency), + isError = false, + isWarning = false, + error = TextReference.EMPTY, + isFiatUnavailable = false, + isValuePasted = false, + onValuePastedTriggerDismiss = {}, + ), + secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading, + ) + } + + private fun BigDecimal.convertToFiatAmount(currency: OnrampCurrency): Amount = Amount( + currencySymbol = currency.name, + value = this, + decimals = currency.precision, + type = AmountType.FiatType(currency.code), + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt new file mode 100644 index 0000000000..8a8c93fef6 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt @@ -0,0 +1,63 @@ +package com.tangem.features.onramp.main.entity.factory.amount + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero +import java.math.BigDecimal + +internal class OnrampAmountFieldChangeConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: String): OnrampMainComponentUM { + val state = currentStateProvider() + if (state !is OnrampMainComponentUM.Content) return state + + if (value.isEmpty()) return state.emptyState() + + val amountState = state.amountBlockState + val amountTextField = amountState.amountFieldModel + val fiatDecimal = value.toBigDecimalOrNull() ?: BigDecimal.ZERO + val isDoneActionEnabled = !fiatDecimal.isNullOrZero() + val amountFieldModel = amountState.amountFieldModel.copy( + fiatValue = value, + fiatAmount = amountTextField.fiatAmount.copy(value = fiatDecimal), + keyboardOptions = KeyboardOptions( + imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, + keyboardType = KeyboardType.Number, + ), + ) + + return state.copy( + amountBlockState = amountState.copy( + amountFieldModel = amountFieldModel, + secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading, + ), + ) + } + + private fun OnrampMainComponentUM.Content.emptyState(): OnrampMainComponentUM.Content { + val amountFieldModel = amountBlockState.amountFieldModel.copy( + value = "", + fiatValue = "", + cryptoAmount = amountBlockState.amountFieldModel.cryptoAmount.copy(value = BigDecimal.ZERO), + fiatAmount = amountBlockState.amountFieldModel.fiatAmount.copy(value = BigDecimal.ZERO), + isError = false, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.None, + keyboardType = KeyboardType.Number, + ), + ) + return copy( + amountBlockState = amountBlockState.copy( + amountFieldModel = amountFieldModel, + secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading, + ), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt new file mode 100644 index 0000000000..fd4c03914f --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt @@ -0,0 +1,38 @@ +package com.tangem.features.onramp.main.entity.factory.amount + +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.domain.tokens.model.AmountType +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.utils.Provider + +internal class OnrampAmountStateFactory(private val currentStateProvider: Provider) { + + private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter( + currentStateProvider = currentStateProvider, + ) + + fun getOnAmountValueChange(value: String) = onrampAmountFieldChangeConverter.convert(value) + + fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampMainComponentUM { + val currentState = currentStateProvider() + if (currentState !is OnrampMainComponentUM.Content) return currentState + + val amountState = currentState.amountBlockState + return currentState.copy( + amountBlockState = amountState.copy( + currencyUM = amountState.currencyUM.copy( + code = currency.code, + iconUrl = currency.image, + precision = currency.precision, + ), + amountFieldModel = amountState.amountFieldModel.copy( + fiatAmount = amountState.amountFieldModel.fiatAmount.copy( + currencySymbol = currency.code, + decimals = currency.precision, + type = AmountType.FiatType(currency.code), + ), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/transformer/OnrampAvailabilityTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/transformer/OnrampAvailabilityTransformer.kt deleted file mode 100644 index 9da2af4521..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/transformer/OnrampAvailabilityTransformer.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.features.onramp.main.entity.transformer - -import arrow.core.Either -import com.tangem.domain.onramp.model.OnrampAvailability -import com.tangem.domain.onramp.model.OnrampCountry -import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig -import com.tangem.features.onramp.main.entity.OnrampMainComponentUM -import com.tangem.utils.transformer.Transformer -import timber.log.Timber - -internal class OnrampAvailabilityTransformer( - private val maybeAvailabilityStatus: Either, - private val openBottomSheet: (OnrampMainBottomSheetConfig) -> Unit, -) : Transformer { - - override fun transform(prevState: OnrampMainComponentUM): OnrampMainComponentUM { - return maybeAvailabilityStatus.fold( - ifLeft = { - Timber.e(it) - prevState - }, - ifRight = { availability -> - when (availability) { - OnrampAvailability.Available -> handleAvailableStatus(prevState) - is OnrampAvailability.ConfirmResidency -> handleShowingConfirmResidency( - prevState = prevState, - country = availability.country, - ) - is OnrampAvailability.NotSupported -> handleShowingConfirmResidency( - prevState = prevState, - country = availability.country, - ) - } - }, - ) - } - - private fun handleAvailableStatus(prevState: OnrampMainComponentUM): OnrampMainComponentUM { - val endButton = prevState.topBarConfig.endButtonUM.copy(enabled = true) - return OnrampMainComponentUM.Content( - topBarConfig = prevState.topBarConfig.copy(endButtonUM = endButton), - buyButtonConfig = prevState.buyButtonConfig, - ) - } - - private fun handleShowingConfirmResidency( - prevState: OnrampMainComponentUM, - country: OnrampCountry, - ): OnrampMainComponentUM { - openBottomSheet.invoke(OnrampMainBottomSheetConfig.ConfirmResidency(country)) - return prevState - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 97f7c33854..50834be67d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -6,33 +6,42 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.domain.onramp.CheckOnrampAvailabilityUseCase +import com.tangem.domain.onramp.GetOnrampCurrencyUseCase +import com.tangem.domain.onramp.model.OnrampAvailability +import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.features.onramp.main.OnrampMainComponent +import com.tangem.features.onramp.main.entity.OnrampIntents import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig import com.tangem.features.onramp.main.entity.OnrampMainComponentUM -import com.tangem.features.onramp.main.entity.transformer.OnrampAvailabilityTransformer +import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory +import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.transformer.Transformer -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject internal class OnrampMainComponentModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase, + private val getOnrampCurrencyUseCase: GetOnrampCurrencyUseCase, paramsContainer: ParamsContainer, -) : Model() { +) : Model(), OnrampIntents { private val params: OnrampMainComponent.Params = paramsContainer.require() + private val stateFactory = OnrampStateFactory( + currentStateProvider = Provider { _state.value }, + cryptoCurrency = params.cryptoCurrency, + onrampIntents = this, + ) + private val amountStateFactory = OnrampAmountStateFactory(currentStateProvider = Provider { _state.value }) + private val _state: MutableStateFlow = MutableStateFlow( - value = OnrampMainComponentUM.InitialLoading( - currency = params.currency, + value = stateFactory.getInitialState( + currency = params.cryptoCurrency.name, onClose = router::pop, - openSettings = params.openSettings, - onBuyClick = ::onBuyClick, ), ) val state: StateFlow get() = _state.asStateFlow() @@ -44,22 +53,50 @@ internal class OnrampMainComponentModel @Inject constructor( private fun checkResidenceCountry() { modelScope.launch { - OnrampAvailabilityTransformer( - maybeAvailabilityStatus = checkOnrampAvailabilityUseCase.invoke(), - openBottomSheet = ::openBottomSheet, - ).let(::updateState) + checkOnrampAvailabilityUseCase.invoke() + .onRight(::handleOnrampAvailability) + .onLeft { Timber.e(it) } } } - @Suppress("EmptyFunctionBlock") - private fun onBuyClick() { + private fun handleOnrampAvailability(availability: OnrampAvailability) { + when (availability) { + is OnrampAvailability.Available -> handleOnrampAvailable(availability.currency) + is OnrampAvailability.ConfirmResidency, + is OnrampAvailability.NotSupported, + -> bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country)) + } } - private fun openBottomSheet(config: OnrampMainBottomSheetConfig) { - bottomSheetNavigation.activate(config) + private fun handleOnrampAvailable(currency: OnrampCurrency) { + _state.update { stateFactory.getReadyState(currency) } + subscribeToCurrencyUpdates() } - private fun updateState(transformer: Transformer) { - _state.update(transformer::transform) + private fun subscribeToCurrencyUpdates() { + getOnrampCurrencyUseCase.invoke() + .onEach { maybeCurrency -> + val currency = maybeCurrency.getOrNull() + if (currency != null) { + _state.update { amountStateFactory.getUpdatedCurrencyState(currency) } + } + } + .launchIn(modelScope) + } + + override fun onAmountValueChanged(value: String) { + _state.update { amountStateFactory.getOnAmountValueChange(value) } + } + + override fun openSettings() { + params.openSettings() + } + + override fun onBuyClick() { + TODO("Not yet implemented") + } + + override fun openCurrenciesList() { + bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt new file mode 100644 index 0000000000..b75e7d3976 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt @@ -0,0 +1,146 @@ +package com.tangem.features.onramp.main.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +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 coil.compose.AsyncImage +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.fields.AmountTextField +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.rememberDecimalFormat +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) { + Column( + modifier = modifier + .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.background.action) + .padding(vertical = TangemTheme.dimens.spacing28), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + OnrampCurrencyIcon( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing24), + currencyUM = state.currencyUM, + ) + OnrampAmountField(amountField = state.amountFieldModel) + OnrampAmountSecondary(state = state.secondaryFieldModel) + } +} + +@Composable +private fun OnrampAmountField(amountField: AmountFieldModel) { + val decimalFormat = rememberDecimalFormat() + val requester = remember { FocusRequester() } + AmountTextField( + value = amountField.fiatValue, + decimals = amountField.fiatAmount.decimals, + visualTransformation = AmountVisualTransformation( + decimals = amountField.fiatAmount.decimals, + symbol = amountField.fiatAmount.currencySymbol, + decimalFormat = decimalFormat, + ), + onValueChange = amountField.onValueChange, + keyboardOptions = amountField.keyboardOptions, + keyboardActions = amountField.keyboardActions, + textStyle = TangemTheme.typography.h2.copy( + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ), + isEnabled = !amountField.isError, + isAutoResize = true, + isValuePasted = amountField.isValuePasted, + onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss, + modifier = Modifier + .focusRequester(requester) + .padding( + top = TangemTheme.dimens.spacing24, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ) + .requiredHeightIn(min = TangemTheme.dimens.size32), + ) + + LaunchedEffect(key1 = Unit) { + delay(timeMillis = 200) + requester.requestFocus() + } +} + +@Composable +private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + contentAlignment = Alignment.Center, + ) { + when (state) { + is OnrampAmountSecondaryFieldUM.Content -> Text( + text = state.amount.resolveReference(), + style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr), + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + is OnrampAmountSecondaryFieldUM.Error -> Text( + text = state.error.resolveReference(), + color = TangemTheme.colors.text.warning, + style = TangemTheme.typography.caption2, + textAlign = TextAlign.Center, + ) + is OnrampAmountSecondaryFieldUM.Loading -> TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(TangemTheme.dimens.size62), + ) + } + } +} + +@Composable +private fun OnrampCurrencyIcon(currencyUM: OnrampCurrencyUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier.clickable(onClick = currencyUM.onClick), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + AsyncImage( + modifier = Modifier + .size(TangemTheme.dimens.size40) + .clip(CircleShape), + model = currencyUM.iconUrl, + contentDescription = null, + ) + Icon( + modifier = Modifier.size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_chevron_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt index bf30582c07..14318c4511 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt @@ -2,7 +2,9 @@ package com.tangem.features.onramp.main.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.FabPosition import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable @@ -23,7 +25,7 @@ import com.tangem.features.onramp.main.entity.OnrampMainComponentUM @Composable internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: Modifier = Modifier) { Scaffold( - modifier = modifier, + modifier = modifier.imePadding(), contentWindowInsets = WindowInsetsZero, containerColor = TangemTheme.colors.background.secondary, topBar = { @@ -42,7 +44,7 @@ internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: .wrapContentHeight() when (state) { is OnrampMainComponentUM.InitialLoading -> InitialLoading(modifier = contentModifier) - is OnrampMainComponentUM.Content -> Content(modifier = contentModifier) + is OnrampMainComponentUM.Content -> Content(modifier = contentModifier, state = state) } }, floatingActionButton = { @@ -85,8 +87,15 @@ private fun InitialLoading(modifier: Modifier = Modifier) { } @Composable -private fun Content(modifier: Modifier = Modifier) { - Box(modifier = modifier) +private fun Content(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .navigationBarsPadding() + .padding(bottom = TangemTheme.dimens.spacing76), + ) { + OnrampAmountContent(state = state.amountBlockState) + } } @Composable diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt index 16d046651d..36fd583a12 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt @@ -63,7 +63,7 @@ internal class DefaultOnrampComponent @AssistedInject constructor( OnrampChild.Main -> onrampMainComponentFactory.create( context = childByContext(componentContext), params = OnrampMainComponent.Params( - currency = params.currency, + cryptoCurrency = params.cryptoCurrency, openSettings = { navigation.push(OnrampChild.Settings) }, ), )