Updated on 2026-08-14
This commit is contained in:
parent
3bac111c2b
commit
854b04624d
24 changed files with 486 additions and 90 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -8,10 +8,13 @@ class CheckOnrampAvailabilityUseCase(private val repository: OnrampRepository) {
|
|||
|
||||
suspend operator fun invoke(): Either<Throwable, OnrampAvailability> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Either<Throwable, OnrampCurrency?>> {
|
||||
return repository.getDefaultCurrency()
|
||||
.map<OnrampCurrency?, Either<Throwable, OnrampCurrency?>> { it.right() }
|
||||
.catch { emit(it.left()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ dependencies {
|
|||
implementation(projects.core.ui)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/* Compose */
|
||||
|
|
|
|||
|
|
@ -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<Params, OnrampComponent>
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.entity
|
||||
package com.tangem.features.onramp.confirmresidency.entity
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Params, OnrampMainComponent>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
interface OnrampIntents {
|
||||
fun onAmountValueChanged(value: String)
|
||||
fun openSettings()
|
||||
fun openCurrenciesList()
|
||||
fun onBuyClick()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ internal sealed interface OnrampMainComponentUM {
|
|||
data class Content(
|
||||
override val topBarConfig: OnrampMainTopBarUM,
|
||||
override val buyButtonConfig: BuyButtonConfig,
|
||||
val amountBlockState: OnrampAmountBlockUM,
|
||||
) : OnrampMainComponentUM
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<OnrampMainComponentUM>,
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
|
@ -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<OnrampMainComponentUM>,
|
||||
) : Converter<String, OnrampMainComponentUM> {
|
||||
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<OnrampMainComponentUM>) {
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Throwable, OnrampAvailability>,
|
||||
private val openBottomSheet: (OnrampMainBottomSheetConfig) -> Unit,
|
||||
) : Transformer<OnrampMainComponentUM> {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<OnrampMainComponentUM> = 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<OnrampMainComponentUM> 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<OnrampMainComponentUM>) {
|
||||
_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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue