Updated on 2026-08-14
This commit is contained in:
parent
d34a83ced5
commit
498e62329a
21 changed files with 1700 additions and 11 deletions
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.tangem.features.onramp.mainv2
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.arkivanov.decompose.ComponentContext
|
||||||
|
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||||
|
import com.arkivanov.decompose.router.slot.childSlot
|
||||||
|
import com.arkivanov.decompose.router.slot.dismiss
|
||||||
|
import com.tangem.core.decompose.context.AppComponentContext
|
||||||
|
import com.tangem.core.decompose.context.childByContext
|
||||||
|
import com.tangem.core.decompose.model.getOrCreateModel
|
||||||
|
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||||
|
import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainBottomSheetConfig
|
||||||
|
import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel
|
||||||
|
import com.tangem.features.onramp.mainv2.ui.OnrampNewMainScreen
|
||||||
|
import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
|
||||||
|
internal class DefaultOnrampV2MainComponent @AssistedInject constructor(
|
||||||
|
@Assisted appComponentContext: AppComponentContext,
|
||||||
|
@Assisted private val params: OnrampV2MainComponent.Params,
|
||||||
|
private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory,
|
||||||
|
private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory,
|
||||||
|
) : OnrampV2MainComponent, AppComponentContext by appComponentContext {
|
||||||
|
|
||||||
|
private val model: OnrampV2MainComponentModel = getOrCreateModel(params)
|
||||||
|
|
||||||
|
private val bottomSheetSlot = childSlot(
|
||||||
|
source = model.bottomSheetNavigation,
|
||||||
|
serializer = null,
|
||||||
|
handleBackButton = false,
|
||||||
|
childFactory = ::bottomSheetChild,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content(modifier: Modifier) {
|
||||||
|
val state by model.state.collectAsState()
|
||||||
|
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||||
|
|
||||||
|
OnrampNewMainScreen(modifier = modifier, state = state)
|
||||||
|
bottomSheet.child?.instance?.BottomSheet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bottomSheetChild(
|
||||||
|
config: OnrampV2MainBottomSheetConfig,
|
||||||
|
componentContext: ComponentContext,
|
||||||
|
): ComposableBottomSheetComponent = when (config) {
|
||||||
|
is OnrampV2MainBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create(
|
||||||
|
context = childByContext(componentContext),
|
||||||
|
params = ConfirmResidencyComponent.Params(
|
||||||
|
userWalletId = params.userWalletId,
|
||||||
|
cryptoCurrency = params.cryptoCurrency,
|
||||||
|
country = config.country,
|
||||||
|
onDismiss = { model.bottomSheetNavigation.dismiss() },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
is OnrampV2MainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create(
|
||||||
|
context = childByContext(componentContext),
|
||||||
|
params = SelectCurrencyComponent.Params(
|
||||||
|
userWallet = model.userWallet,
|
||||||
|
cryptoCurrency = params.cryptoCurrency,
|
||||||
|
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory : OnrampV2MainComponent.Factory {
|
||||||
|
override fun create(
|
||||||
|
context: AppComponentContext,
|
||||||
|
params: OnrampV2MainComponent.Params,
|
||||||
|
): DefaultOnrampV2MainComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package com.tangem.features.onramp.newmain
|
package com.tangem.features.onramp.mainv2
|
||||||
|
|
||||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||||
|
|
||||||
class DefaultOnrampNewMainFeatureToggle(
|
class DefaultOnrampV2MainFeatureToggle(
|
||||||
private val featureTogglesManager: FeatureTogglesManager,
|
private val featureTogglesManager: FeatureTogglesManager,
|
||||||
) : OnrampNewMainFeatureToggle {
|
) : OnrampV2MainFeatureToggle {
|
||||||
override val isOnrampNewMainEnabled: Boolean
|
override val isOnrampNewMainEnabled: Boolean
|
||||||
get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_RECEIVE_ENABLED")
|
get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_RECEIVE_ENABLED")
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.tangem.features.onramp.newmain
|
package com.tangem.features.onramp.mainv2
|
||||||
|
|
||||||
import com.tangem.core.decompose.factory.ComponentFactory
|
import com.tangem.core.decompose.factory.ComponentFactory
|
||||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||||
|
|
@ -7,7 +7,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||||
import com.tangem.domain.onramp.model.OnrampSource
|
import com.tangem.domain.onramp.model.OnrampSource
|
||||||
|
|
||||||
internal interface OnrampNewMainComponent : ComposableContentComponent {
|
internal interface OnrampV2MainComponent : ComposableContentComponent {
|
||||||
|
|
||||||
data class Params(
|
data class Params(
|
||||||
val userWalletId: UserWalletId,
|
val userWalletId: UserWalletId,
|
||||||
|
|
@ -17,5 +17,5 @@ internal interface OnrampNewMainComponent : ComposableContentComponent {
|
||||||
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
|
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface Factory : ComponentFactory<Params, OnrampNewMainComponent>
|
interface Factory : ComponentFactory<Params, OnrampV2MainComponent>
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.tangem.features.onramp.mainv2
|
||||||
|
|
||||||
|
internal interface OnrampV2MainFeatureToggle {
|
||||||
|
val isOnrampNewMainEnabled: Boolean
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.di
|
||||||
|
|
||||||
|
import com.tangem.core.decompose.di.ModelComponent
|
||||||
|
import com.tangem.core.decompose.model.Model
|
||||||
|
import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.multibindings.ClassKey
|
||||||
|
import dagger.multibindings.IntoMap
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(ModelComponent::class)
|
||||||
|
internal interface OnrampMainV2ComponentModelModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ClassKey(OnrampV2MainComponentModel::class)
|
||||||
|
fun bindOnrampSelectCountryModel(model: OnrampV2MainComponentModel): Model
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.di
|
||||||
|
|
||||||
|
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||||
|
import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainComponent
|
||||||
|
import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainFeatureToggle
|
||||||
|
import com.tangem.features.onramp.mainv2.OnrampV2MainComponent
|
||||||
|
import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal interface OnrampNewMainComponentModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
fun bindOnrampV2MainComponentFactory(factory: DefaultOnrampV2MainComponent.Factory): OnrampV2MainComponent.Factory
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal object FeatureToggleModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideOnrampV2MainFeatureToggle(featureTogglesManager: FeatureTogglesManager): OnrampV2MainFeatureToggle {
|
||||||
|
return DefaultOnrampV2MainFeatureToggle(featureTogglesManager = featureTogglesManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.entity
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
internal sealed interface OnrampOffersBlockUM {
|
||||||
|
|
||||||
|
val isBlockVisible: Boolean
|
||||||
|
|
||||||
|
data object Empty : OnrampOffersBlockUM {
|
||||||
|
override val isBlockVisible: Boolean
|
||||||
|
get() = false
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Loading(
|
||||||
|
override val isBlockVisible: Boolean,
|
||||||
|
) : OnrampOffersBlockUM
|
||||||
|
|
||||||
|
data class Content(
|
||||||
|
override val isBlockVisible: Boolean,
|
||||||
|
val offers: ImmutableList<OnrampOfferUM>,
|
||||||
|
) : OnrampOffersBlockUM
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class OnrampOfferUM(
|
||||||
|
val category: OnrampOfferCategory,
|
||||||
|
val advantages: OnrampOfferAdvantages,
|
||||||
|
val paymentMethod: OnrampPaymentMethod,
|
||||||
|
val providerId: String,
|
||||||
|
val providerName: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal enum class OnrampOfferCategory {
|
||||||
|
RecentlyUsed, Recommended
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum class OnrampOfferAdvantages {
|
||||||
|
Default, BestRate, Fastest
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.entity
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
internal data class OnrampNewAmountBlockUM(
|
||||||
|
val currencyUM: OnrampNewCurrencyUM,
|
||||||
|
val amountFieldModel: AmountFieldModel,
|
||||||
|
val secondaryFieldModel: OnrampNewAmountSecondaryFieldUM,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class OnrampNewCurrencyUM(
|
||||||
|
val unit: String,
|
||||||
|
val code: String,
|
||||||
|
val iconUrl: String?,
|
||||||
|
val precision: Int,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
internal sealed interface OnrampNewAmountSecondaryFieldUM {
|
||||||
|
data object Loading : OnrampNewAmountSecondaryFieldUM
|
||||||
|
data class Content(val amount: TextReference) : OnrampNewAmountSecondaryFieldUM
|
||||||
|
data class Error(val error: TextReference) : OnrampNewAmountSecondaryFieldUM
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed interface OnrampV2AmountButtonUMState {
|
||||||
|
data class Loaded(val amountButtons: ImmutableList<OnrampAmountButtonUM>) : OnrampV2AmountButtonUMState
|
||||||
|
data object None : OnrampV2AmountButtonUMState
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class OnrampAmountButtonUM(
|
||||||
|
val value: Int,
|
||||||
|
val currency: String,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.entity
|
||||||
|
|
||||||
|
import com.tangem.domain.onramp.model.OnrampCountry
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface OnrampV2MainBottomSheetConfig {
|
||||||
|
@Serializable
|
||||||
|
data class ConfirmResidency(val country: OnrampCountry) : OnrampV2MainBottomSheetConfig
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object CurrenciesList : OnrampV2MainBottomSheetConfig
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.entity
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.tangem.common.ui.notifications.NotificationUM
|
||||||
|
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
internal sealed interface OnrampV2MainComponentUM {
|
||||||
|
|
||||||
|
val topBarConfig: OnrampV2MainTopBarUM
|
||||||
|
val continueButtonConfig: ContinueButtonUM
|
||||||
|
val errorNotification: NotificationUM?
|
||||||
|
|
||||||
|
data class InitialLoading(
|
||||||
|
override val topBarConfig: OnrampV2MainTopBarUM,
|
||||||
|
override val continueButtonConfig: ContinueButtonUM,
|
||||||
|
override val errorNotification: NotificationUM?,
|
||||||
|
) : OnrampV2MainComponentUM
|
||||||
|
|
||||||
|
data class Content(
|
||||||
|
override val topBarConfig: OnrampV2MainTopBarUM,
|
||||||
|
override val continueButtonConfig: ContinueButtonUM,
|
||||||
|
override val errorNotification: NotificationUM?,
|
||||||
|
val amountBlockState: OnrampNewAmountBlockUM,
|
||||||
|
val offersBlockState: OnrampOffersBlockUM,
|
||||||
|
val onrampAmountButtonUMState: OnrampV2AmountButtonUMState,
|
||||||
|
val onrampProviderState: OnrampV2ProvidersUM,
|
||||||
|
) : OnrampV2MainComponentUM
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class ContinueButtonUM(
|
||||||
|
val text: TextReference,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
val enabled: Boolean,
|
||||||
|
val showProgress: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class OnrampV2MainTopBarUM(
|
||||||
|
val title: TextReference,
|
||||||
|
val startButtonUM: TopAppBarButtonUM,
|
||||||
|
val endButtonUM: TopAppBarButtonUM,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.entity
|
||||||
|
|
||||||
|
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||||
|
|
||||||
|
sealed interface OnrampV2ProvidersUM {
|
||||||
|
|
||||||
|
data object Empty : OnrampV2ProvidersUM
|
||||||
|
|
||||||
|
data object Loading : OnrampV2ProvidersUM
|
||||||
|
|
||||||
|
data class Content(
|
||||||
|
val providerId: String,
|
||||||
|
val paymentMethod: OnrampPaymentMethod,
|
||||||
|
) : OnrampV2ProvidersUM
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.entity.converter
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||||
|
import com.tangem.core.ui.format.bigdecimal.format
|
||||||
|
import com.tangem.core.ui.utils.parseBigDecimalOrNull
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.features.onramp.main.entity.OnrampIntents
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.*
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory
|
||||||
|
import com.tangem.utils.Provider
|
||||||
|
import com.tangem.utils.converter.Converter
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
internal class OnrampV2AmountFieldChangeConverter(
|
||||||
|
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||||
|
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||||
|
private val onrampIntents: OnrampIntents,
|
||||||
|
private val cryptoCurrency: CryptoCurrency,
|
||||||
|
) : Converter<String, OnrampV2MainComponentUM> {
|
||||||
|
|
||||||
|
override fun convert(value: String): OnrampV2MainComponentUM {
|
||||||
|
val state = currentStateProvider()
|
||||||
|
if (state !is OnrampV2MainComponentUM.Content) return state
|
||||||
|
|
||||||
|
if (value.isEmpty()) return state.emptyState()
|
||||||
|
|
||||||
|
val amountState = state.amountBlockState
|
||||||
|
val amountTextField = amountState.amountFieldModel
|
||||||
|
val fiatDecimal = value.parseBigDecimalOrNull() ?: BigDecimal.ZERO
|
||||||
|
val amountFieldModel = amountState.amountFieldModel.copy(
|
||||||
|
fiatValue = value,
|
||||||
|
fiatAmount = amountTextField.fiatAmount.copy(value = fiatDecimal),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
)
|
||||||
|
|
||||||
|
return state.copy(
|
||||||
|
amountBlockState = amountState.copy(
|
||||||
|
amountFieldModel = amountFieldModel,
|
||||||
|
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Loading,
|
||||||
|
),
|
||||||
|
continueButtonConfig = state.continueButtonConfig.copy(enabled = false),
|
||||||
|
onrampProviderState = OnrampV2ProvidersUM.Loading,
|
||||||
|
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun OnrampV2MainComponentUM.Content.emptyState(): OnrampV2MainComponentUM.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 = OnrampNewAmountSecondaryFieldUM.Content(
|
||||||
|
stringReference(
|
||||||
|
BigDecimal.ZERO.format {
|
||||||
|
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
continueButtonConfig = continueButtonConfig.copy(enabled = false),
|
||||||
|
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||||
|
onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton(
|
||||||
|
currencySymbol = amountBlockState.currencyUM.unit,
|
||||||
|
currencyCode = amountBlockState.currencyUM.code,
|
||||||
|
onAmountValueChanged = onrampIntents::onAmountValueChanged,
|
||||||
|
),
|
||||||
|
onrampProviderState = OnrampV2ProvidersUM.Empty,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.entity.factory
|
||||||
|
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
|
internal class OnrampAmountButtonUMStateFactory {
|
||||||
|
|
||||||
|
private val defaultPreselectedAmount = listOf(50, 100, 200, 300, 500)
|
||||||
|
|
||||||
|
fun createOnrampAmountActionButton(
|
||||||
|
currencyCode: String,
|
||||||
|
currencySymbol: String,
|
||||||
|
onAmountValueChanged: (String) -> Unit,
|
||||||
|
): OnrampV2AmountButtonUMState {
|
||||||
|
return when (currencyCode) {
|
||||||
|
USD_CODE, EUR_CODE -> {
|
||||||
|
val buttons = defaultPreselectedAmount.map { value ->
|
||||||
|
OnrampAmountButtonUM(
|
||||||
|
value = value,
|
||||||
|
currency = currencySymbol,
|
||||||
|
onClick = { onAmountValueChanged(value.toString()) },
|
||||||
|
)
|
||||||
|
}.toPersistentList()
|
||||||
|
OnrampV2AmountButtonUMState.Loaded(buttons)
|
||||||
|
}
|
||||||
|
else -> OnrampV2AmountButtonUMState.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val USD_CODE = "USD"
|
||||||
|
private const val EUR_CODE = "EUR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.entity.factory
|
||||||
|
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
|
import com.tangem.core.ui.extensions.combinedReference
|
||||||
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
|
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||||
|
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||||
|
import com.tangem.core.ui.format.bigdecimal.format
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
|
||||||
|
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||||
|
import com.tangem.domain.onramp.model.OnrampQuote
|
||||||
|
import com.tangem.domain.onramp.model.error.OnrampError
|
||||||
|
import com.tangem.domain.tokens.model.AmountType
|
||||||
|
import com.tangem.features.onramp.impl.R
|
||||||
|
import com.tangem.features.onramp.main.entity.OnrampIntents
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.*
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.converter.OnrampV2AmountFieldChangeConverter
|
||||||
|
import com.tangem.utils.Provider
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
internal class OnrampV2AmountStateFactory(
|
||||||
|
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||||
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
|
private val onrampIntents: OnrampIntents,
|
||||||
|
private val cryptoCurrency: CryptoCurrency,
|
||||||
|
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val onrampAmountFieldChangeConverter: OnrampV2AmountFieldChangeConverter by lazy(
|
||||||
|
mode = LazyThreadSafetyMode.NONE,
|
||||||
|
) {
|
||||||
|
OnrampV2AmountFieldChangeConverter(
|
||||||
|
currentStateProvider = currentStateProvider,
|
||||||
|
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||||
|
onrampIntents = onrampIntents,
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOnAmountValueChange(value: String): OnrampV2MainComponentUM {
|
||||||
|
return onrampAmountFieldChangeConverter.convert(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampV2MainComponentUM {
|
||||||
|
val currentState = currentStateProvider()
|
||||||
|
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||||
|
|
||||||
|
val amountState = currentState.amountBlockState
|
||||||
|
|
||||||
|
return currentState.copy(
|
||||||
|
amountBlockState = amountState.copy(
|
||||||
|
currencyUM = amountState.currencyUM.copy(
|
||||||
|
unit = currency.unit,
|
||||||
|
code = currency.code,
|
||||||
|
iconUrl = currency.image,
|
||||||
|
precision = currency.precision,
|
||||||
|
),
|
||||||
|
amountFieldModel = amountState.amountFieldModel.copy(
|
||||||
|
isError = false,
|
||||||
|
fiatAmount = amountState.amountFieldModel.fiatAmount.copy(
|
||||||
|
currencySymbol = currency.unit,
|
||||||
|
decimals = currency.precision,
|
||||||
|
type = AmountType.FiatType(currency.code),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton(
|
||||||
|
currencyCode = currency.code,
|
||||||
|
currencySymbol = currency.unit,
|
||||||
|
onAmountValueChanged = onrampIntents::onAmountValueChanged,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAmountSecondaryLoadingState(): OnrampV2MainComponentUM {
|
||||||
|
val currentState = currentStateProvider()
|
||||||
|
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||||
|
|
||||||
|
val amountState = currentState.amountBlockState
|
||||||
|
|
||||||
|
return currentState.copy(
|
||||||
|
amountBlockState = amountState.copy(
|
||||||
|
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Loading,
|
||||||
|
),
|
||||||
|
offersBlockState = OnrampOffersBlockUM.Loading(isBlockVisible = false),
|
||||||
|
continueButtonConfig = currentState.continueButtonConfig.copy(enabled = false),
|
||||||
|
errorNotification = null,
|
||||||
|
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||||
|
onrampProviderState = OnrampV2ProvidersUM.Loading,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampV2MainComponentUM {
|
||||||
|
val currentState = currentStateProvider()
|
||||||
|
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||||
|
|
||||||
|
val amountState = currentState.amountBlockState
|
||||||
|
if (amountState.amountFieldModel.fiatValue.isEmpty()) return currentState
|
||||||
|
|
||||||
|
return currentState.copy(
|
||||||
|
amountBlockState = amountState.copy(
|
||||||
|
amountFieldModel = amountState.amountFieldModel.copy(isError = false),
|
||||||
|
secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState) ?: amountState.secondaryFieldModel,
|
||||||
|
),
|
||||||
|
continueButtonConfig = currentState.continueButtonConfig.copy(
|
||||||
|
enabled = quote is OnrampQuote.Data,
|
||||||
|
onClick = onrampIntents::onContinueClick,
|
||||||
|
),
|
||||||
|
errorNotification = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUpdatedProviderState(selectedQuote: OnrampQuote): OnrampV2MainComponentUM {
|
||||||
|
val currentState = currentStateProvider()
|
||||||
|
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||||
|
|
||||||
|
analyticsEventHandler.send(
|
||||||
|
OnrampAnalyticsEvent.ProviderCalculated(
|
||||||
|
providerName = selectedQuote.provider.info.name,
|
||||||
|
tokenSymbol = cryptoCurrency.symbol,
|
||||||
|
paymentMethod = selectedQuote.paymentMethod.name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return currentState.copy(
|
||||||
|
onrampProviderState = selectedQuote.toProviderBlockState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAmountSecondaryResetState(): OnrampV2MainComponentUM {
|
||||||
|
val currentState = currentStateProvider()
|
||||||
|
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||||
|
|
||||||
|
val amountState = currentState.amountBlockState
|
||||||
|
|
||||||
|
if (amountState.secondaryFieldModel is OnrampNewAmountSecondaryFieldUM.Content) return currentState
|
||||||
|
|
||||||
|
return currentState.copy(
|
||||||
|
amountBlockState = amountState.copy(
|
||||||
|
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content(
|
||||||
|
amount = stringReference(
|
||||||
|
BigDecimal.ZERO.format {
|
||||||
|
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||||
|
errorNotification = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getShowProvidersState(): OnrampV2MainComponentUM {
|
||||||
|
val currentState = currentStateProvider()
|
||||||
|
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||||
|
|
||||||
|
return when (currentState.offersBlockState) {
|
||||||
|
is OnrampOffersBlockUM.Content -> {
|
||||||
|
currentState.copy(
|
||||||
|
offersBlockState = currentState.offersBlockState.copy(isBlockVisible = true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OnrampOffersBlockUM.Empty,
|
||||||
|
is OnrampOffersBlockUM.Loading,
|
||||||
|
-> currentState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun OnrampQuote.toProviderBlockState(): OnrampV2ProvidersUM {
|
||||||
|
return OnrampV2ProvidersUM.Content(
|
||||||
|
paymentMethod = paymentMethod,
|
||||||
|
providerId = provider.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun OnrampQuote.toSecondaryFieldUiModel(
|
||||||
|
amountState: OnrampNewAmountBlockUM,
|
||||||
|
): OnrampNewAmountSecondaryFieldUM? {
|
||||||
|
return when (this) {
|
||||||
|
is OnrampQuote.Error -> null
|
||||||
|
is OnrampQuote.Data -> {
|
||||||
|
val amount = toAmount.value.format {
|
||||||
|
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||||
|
}
|
||||||
|
val contentAmount = combinedReference(stringReference("\u007E"), stringReference(amount))
|
||||||
|
OnrampNewAmountSecondaryFieldUM.Content(contentAmount)
|
||||||
|
}
|
||||||
|
is OnrampQuote.AmountError -> this.toSecondaryFieldUiModel(amountState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun OnrampQuote.AmountError.toSecondaryFieldUiModel(
|
||||||
|
amountState: OnrampNewAmountBlockUM,
|
||||||
|
): OnrampNewAmountSecondaryFieldUM.Error {
|
||||||
|
val amount = error.requiredAmount.format {
|
||||||
|
fiat(
|
||||||
|
fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol,
|
||||||
|
fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val errorTextRes = when (error) {
|
||||||
|
is OnrampError.AmountError.TooBigError -> {
|
||||||
|
analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError)
|
||||||
|
R.string.onramp_max_amount_restriction
|
||||||
|
}
|
||||||
|
is OnrampError.AmountError.TooSmallError -> {
|
||||||
|
analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError)
|
||||||
|
R.string.onramp_min_amount_restriction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return OnrampNewAmountSecondaryFieldUM.Error(
|
||||||
|
resourceReference(
|
||||||
|
errorTextRes,
|
||||||
|
wrappedList(amount),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.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.common.ui.notifications.NotificationUM
|
||||||
|
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.combinedReference
|
||||||
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||||
|
import com.tangem.core.ui.format.bigdecimal.format
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||||
|
import com.tangem.domain.onramp.model.error.OnrampError
|
||||||
|
import com.tangem.domain.tokens.model.Amount
|
||||||
|
import com.tangem.domain.tokens.model.AmountType
|
||||||
|
import com.tangem.domain.tokens.model.convertToAmount
|
||||||
|
import com.tangem.features.onramp.impl.R
|
||||||
|
import com.tangem.features.onramp.main.entity.OnrampIntents
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.*
|
||||||
|
import com.tangem.utils.Provider
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
internal class OnrampV2StateFactory(
|
||||||
|
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||||
|
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||||
|
private val cryptoCurrency: CryptoCurrency,
|
||||||
|
private val onrampIntents: OnrampIntents,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun getInitialState(
|
||||||
|
currency: String,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
openSettings: () -> Unit,
|
||||||
|
): OnrampV2MainComponentUM.InitialLoading {
|
||||||
|
return OnrampV2MainComponentUM.InitialLoading(
|
||||||
|
errorNotification = null,
|
||||||
|
topBarConfig = OnrampV2MainTopBarUM(
|
||||||
|
title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")),
|
||||||
|
startButtonUM = TopAppBarButtonUM.Close(
|
||||||
|
onCloseClick = onClose,
|
||||||
|
enabled = true,
|
||||||
|
),
|
||||||
|
endButtonUM = TopAppBarButtonUM.Icon(
|
||||||
|
iconRes = R.drawable.ic_more_vertical_24,
|
||||||
|
onClicked = openSettings,
|
||||||
|
enabled = false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
continueButtonConfig = ContinueButtonUM(
|
||||||
|
text = resourceReference(R.string.common_continue),
|
||||||
|
onClick = {},
|
||||||
|
enabled = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getReadyState(currency: OnrampCurrency): OnrampV2MainComponentUM.Content {
|
||||||
|
val state = currentStateProvider()
|
||||||
|
|
||||||
|
val endButton = when (val button = state.topBarConfig.endButtonUM) {
|
||||||
|
is TopAppBarButtonUM.Icon -> button.copy(enabled = true)
|
||||||
|
is TopAppBarButtonUM.Text -> button.copy(enabled = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val initialAmountBlockState = getInitialAmountBlockState(currency)
|
||||||
|
|
||||||
|
return OnrampV2MainComponentUM.Content(
|
||||||
|
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||||
|
continueButtonConfig = ContinueButtonUM(
|
||||||
|
text = resourceReference(R.string.common_continue),
|
||||||
|
onClick = onrampIntents::onContinueClick,
|
||||||
|
enabled = false,
|
||||||
|
),
|
||||||
|
amountBlockState = initialAmountBlockState,
|
||||||
|
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||||
|
errorNotification = null,
|
||||||
|
onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton(
|
||||||
|
currencyCode = currency.code,
|
||||||
|
currencySymbol = currency.unit,
|
||||||
|
onAmountValueChanged = onrampIntents::onAmountValueChanged,
|
||||||
|
),
|
||||||
|
onrampProviderState = OnrampV2ProvidersUM.Empty,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOnrampErrorState(onrampError: OnrampError): OnrampV2MainComponentUM {
|
||||||
|
return when (onrampError) {
|
||||||
|
OnrampError.PairsNotFound -> getNoPairsErrorState()
|
||||||
|
is OnrampError.DataError -> getErrorState(
|
||||||
|
errorCode = onrampError.code,
|
||||||
|
onRefresh = onrampIntents::onRefresh,
|
||||||
|
)
|
||||||
|
is OnrampError.DomainError -> getErrorState(onRefresh = onrampIntents::onRefresh)
|
||||||
|
is OnrampError.AmountError.TooBigError,
|
||||||
|
is OnrampError.AmountError.TooSmallError,
|
||||||
|
OnrampError.RedirectError.VerificationFailed,
|
||||||
|
OnrampError.RedirectError.WrongRequestId,
|
||||||
|
-> currentStateProvider() // ignore error state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNoPairsErrorState(): OnrampV2MainComponentUM {
|
||||||
|
val state = currentStateProvider()
|
||||||
|
val contentState = state as? OnrampV2MainComponentUM.Content ?: return state
|
||||||
|
|
||||||
|
return contentState.copy(
|
||||||
|
continueButtonConfig = contentState.continueButtonConfig.copy(enabled = false),
|
||||||
|
amountBlockState = contentState.amountBlockState.copy(
|
||||||
|
amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true),
|
||||||
|
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Error(
|
||||||
|
error = resourceReference(R.string.onramp_no_available_providers),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||||
|
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampV2MainComponentUM {
|
||||||
|
val state = currentStateProvider()
|
||||||
|
val endButton = when (val button = state.topBarConfig.endButtonUM) {
|
||||||
|
is TopAppBarButtonUM.Icon -> button.copy(enabled = true)
|
||||||
|
is TopAppBarButtonUM.Text -> button.copy(enabled = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return when (state) {
|
||||||
|
is OnrampV2MainComponentUM.Content -> state.copy(
|
||||||
|
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||||
|
continueButtonConfig = state.continueButtonConfig.copy(enabled = false),
|
||||||
|
amountBlockState = state.amountBlockState.copy(
|
||||||
|
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Content(
|
||||||
|
stringReference(
|
||||||
|
BigDecimal.ZERO.format {
|
||||||
|
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||||
|
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||||
|
errorCode = errorCode,
|
||||||
|
onRefresh = onRefresh,
|
||||||
|
),
|
||||||
|
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||||
|
onrampProviderState = OnrampV2ProvidersUM.Empty,
|
||||||
|
)
|
||||||
|
is OnrampV2MainComponentUM.InitialLoading -> state.copy(
|
||||||
|
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||||
|
errorCode = errorCode,
|
||||||
|
onRefresh = onRefresh,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampNewAmountBlockUM {
|
||||||
|
return OnrampNewAmountBlockUM(
|
||||||
|
currencyUM = OnrampNewCurrencyUM(
|
||||||
|
code = currency.code,
|
||||||
|
iconUrl = currency.image,
|
||||||
|
precision = currency.precision,
|
||||||
|
onClick = onrampIntents::openCurrenciesList,
|
||||||
|
unit = currency.unit,
|
||||||
|
),
|
||||||
|
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 = OnrampNewAmountSecondaryFieldUM.Content(
|
||||||
|
stringReference(
|
||||||
|
BigDecimal.ZERO.format {
|
||||||
|
crypto(cryptoCurrency = cryptoCurrency, ignoreSymbolPosition = true)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BigDecimal.convertToFiatAmount(currency: OnrampCurrency): Amount = Amount(
|
||||||
|
currencySymbol = currency.unit,
|
||||||
|
value = this,
|
||||||
|
decimals = currency.precision,
|
||||||
|
type = AmountType.FiatType(currency.code),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,359 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||||
|
import com.arkivanov.decompose.router.slot.activate
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
|
import com.tangem.core.decompose.model.Model
|
||||||
|
import com.tangem.core.decompose.model.ParamsContainer
|
||||||
|
import com.tangem.core.decompose.navigation.Router
|
||||||
|
import com.tangem.core.ui.components.fields.InputManager
|
||||||
|
import com.tangem.domain.onramp.*
|
||||||
|
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
|
||||||
|
import com.tangem.domain.onramp.model.OnrampAvailability
|
||||||
|
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||||
|
import com.tangem.domain.onramp.model.OnrampQuote
|
||||||
|
import com.tangem.domain.onramp.model.error.OnrampError
|
||||||
|
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||||
|
import com.tangem.features.onramp.main.entity.OnrampIntents
|
||||||
|
import com.tangem.features.onramp.main.entity.OnrampLastUpdate
|
||||||
|
import com.tangem.features.onramp.mainv2.OnrampV2MainComponent
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.*
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2AmountStateFactory
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2StateFactory
|
||||||
|
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
|
||||||
|
import com.tangem.utils.Provider
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import com.tangem.utils.isNullOrZero
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
internal class OnrampV2MainComponentModel @Inject constructor(
|
||||||
|
override val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
|
private val router: Router,
|
||||||
|
private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase,
|
||||||
|
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,
|
||||||
|
private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase,
|
||||||
|
private val fetchQuotesUseCase: OnrampFetchQuotesUseCase,
|
||||||
|
private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase,
|
||||||
|
private val fetchPairsUseCase: OnrampFetchPairsUseCase,
|
||||||
|
private val amountInputManager: InputManager,
|
||||||
|
paramsContainer: ParamsContainer,
|
||||||
|
getWalletsUseCase: GetWalletsUseCase,
|
||||||
|
) : Model(), OnrampIntents {
|
||||||
|
|
||||||
|
val params = paramsContainer.require<OnrampV2MainComponent.Params>()
|
||||||
|
|
||||||
|
private var loadQuotesJob: Job? = null
|
||||||
|
|
||||||
|
private val lastUpdateState = mutableStateOf<OnrampLastUpdate?>(null)
|
||||||
|
|
||||||
|
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
OnrampAmountButtonUMStateFactory()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val stateFactory = OnrampV2StateFactory(
|
||||||
|
currentStateProvider = Provider { _state.value },
|
||||||
|
cryptoCurrency = params.cryptoCurrency,
|
||||||
|
onrampIntents = this,
|
||||||
|
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val amountStateFactory = OnrampV2AmountStateFactory(
|
||||||
|
currentStateProvider = Provider { _state.value },
|
||||||
|
analyticsEventHandler = analyticsEventHandler,
|
||||||
|
onrampIntents = this,
|
||||||
|
cryptoCurrency = params.cryptoCurrency,
|
||||||
|
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val _state: MutableStateFlow<OnrampV2MainComponentUM> = MutableStateFlow(
|
||||||
|
value = stateFactory.getInitialState(
|
||||||
|
currency = params.cryptoCurrency.name,
|
||||||
|
onClose = ::onCloseClick,
|
||||||
|
openSettings = ::openSettings,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val state: StateFlow<OnrampV2MainComponentUM> get() = _state.asStateFlow()
|
||||||
|
val bottomSheetNavigation: SlotNavigation<OnrampV2MainBottomSheetConfig> = SlotNavigation()
|
||||||
|
val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
|
||||||
|
|
||||||
|
init {
|
||||||
|
modelScope.launch {
|
||||||
|
clearOnrampCacheUseCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
sendScreenOpenAnalytics()
|
||||||
|
checkResidenceCountry()
|
||||||
|
subscribeToAmountChanges()
|
||||||
|
subscribeToCountryAndCurrencyUpdates()
|
||||||
|
subscribeToQuotesUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
modelScope.launch { clearOnrampCacheUseCase.invoke() }
|
||||||
|
loadQuotesJob?.cancel()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAmountValueChanged(value: String) {
|
||||||
|
_state.update { amountStateFactory.getOnAmountValueChange(value) }
|
||||||
|
modelScope.launch { amountInputManager.update(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openSettings() {
|
||||||
|
params.openSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openCurrenciesList() {
|
||||||
|
analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened)
|
||||||
|
bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.CurrenciesList)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBuyClick(quote: OnrampProviderWithQuote.Data) {
|
||||||
|
// TODO in [REDACTED_TASK_KEY] to be continued
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openProviders() {
|
||||||
|
// TODO in [REDACTED_TASK_KEY] to be continued
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRefresh() {
|
||||||
|
_state.update {
|
||||||
|
stateFactory.getInitialState(
|
||||||
|
currency = params.cryptoCurrency.name,
|
||||||
|
onClose = router::pop,
|
||||||
|
openSettings = ::openSettings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
loadQuotesJob?.cancel()
|
||||||
|
modelScope.launch {
|
||||||
|
clearOnrampCacheUseCase.invoke()
|
||||||
|
checkResidenceCountry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLinkClick(link: String) {
|
||||||
|
// TODO in [REDACTED_TASK_KEY] will be deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onContinueClick() {
|
||||||
|
val currentState = _state.value
|
||||||
|
if (currentState is OnrampV2MainComponentUM.Content) {
|
||||||
|
_state.update { amountStateFactory.getShowProvidersState() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkResidenceCountry() {
|
||||||
|
modelScope.launch {
|
||||||
|
checkOnrampAvailabilityUseCase(userWallet)
|
||||||
|
.onRight(::handleOnrampAvailability)
|
||||||
|
.onLeft(::handleOnrampError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleOnrampAvailability(availability: OnrampAvailability) {
|
||||||
|
when (availability) {
|
||||||
|
is OnrampAvailability.Available -> Unit
|
||||||
|
is OnrampAvailability.ConfirmResidency,
|
||||||
|
is OnrampAvailability.NotSupported,
|
||||||
|
-> bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.ConfirmResidency(availability.country))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCloseClick() {
|
||||||
|
analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp)
|
||||||
|
router.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun subscribeToAmountChanges() = modelScope.launch {
|
||||||
|
amountInputManager.query
|
||||||
|
.filter(String::isNotEmpty)
|
||||||
|
.collectLatest { _ ->
|
||||||
|
_state.update { amountStateFactory.getAmountSecondaryLoadingState() }
|
||||||
|
loadQuotes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun subscribeToCountryAndCurrencyUpdates() {
|
||||||
|
getOnrampCountryUseCase.invoke()
|
||||||
|
.onEach { maybeCountry ->
|
||||||
|
maybeCountry.fold(
|
||||||
|
ifLeft = ::handleOnrampError,
|
||||||
|
ifRight = { country ->
|
||||||
|
if (country == null) return@onEach
|
||||||
|
_state.update {
|
||||||
|
when (it) {
|
||||||
|
is OnrampV2MainComponentUM.Content -> {
|
||||||
|
amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency)
|
||||||
|
}
|
||||||
|
is OnrampV2MainComponentUM.InitialLoading -> {
|
||||||
|
stateFactory.getReadyState(country.defaultCurrency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatePairsAndQuotes()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.launchIn(modelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun subscribeToQuotesUpdate() {
|
||||||
|
getOnrampQuotesUseCase.invoke()
|
||||||
|
.conflate()
|
||||||
|
.onEach { maybeQuotes ->
|
||||||
|
maybeQuotes.fold(
|
||||||
|
ifLeft = ::handleOnrampError,
|
||||||
|
ifRight = ::handleQuoteResult,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.launchIn(modelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleQuoteResult(quotes: List<OnrampQuote>) {
|
||||||
|
sendOnrampQuotesErrorAnalytic(quotes)
|
||||||
|
|
||||||
|
val quote = selectOrUpdateQuote(quotes)
|
||||||
|
|
||||||
|
if (quote == null) {
|
||||||
|
_state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||||
|
lastUpdateState.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectOrUpdateQuote(quotes: List<OnrampQuote>): OnrampQuote? {
|
||||||
|
val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error }
|
||||||
|
|
||||||
|
// Check if amount, country or currency has changed
|
||||||
|
val newQuote = if (checkLastInputState(quoteToCheck)) {
|
||||||
|
quoteToCheck
|
||||||
|
} else {
|
||||||
|
val state = state.value as? OnrampV2MainComponentUM.Content
|
||||||
|
val providerState = state?.onrampProviderState as? OnrampV2ProvidersUM.Content
|
||||||
|
|
||||||
|
// Get current selected quote to update
|
||||||
|
val lastSelectedQuote = quotes.firstOrNull {
|
||||||
|
it.provider.id == providerState?.providerId &&
|
||||||
|
it.paymentMethod.id == providerState.paymentMethod.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastSelectedQuote is OnrampQuote.Error) {
|
||||||
|
quoteToCheck
|
||||||
|
} else {
|
||||||
|
lastSelectedQuote
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newQuote?.let { updateProvider(newQuote) }
|
||||||
|
|
||||||
|
return newQuote
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRetryQuotes() {
|
||||||
|
_state.update {
|
||||||
|
(it as? OnrampV2MainComponentUM.Content)?.copy(
|
||||||
|
errorNotification = null,
|
||||||
|
onrampProviderState = OnrampV2ProvidersUM.Loading,
|
||||||
|
offersBlockState = OnrampOffersBlockUM.Loading(isBlockVisible = false),
|
||||||
|
amountBlockState = it.amountBlockState.copy(
|
||||||
|
secondaryFieldModel = OnrampNewAmountSecondaryFieldUM.Loading,
|
||||||
|
),
|
||||||
|
) ?: it
|
||||||
|
}
|
||||||
|
loadQuotes()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updatePairsAndQuotes() {
|
||||||
|
val state = state.value as? OnrampV2MainComponentUM.Content
|
||||||
|
|
||||||
|
if (!state?.amountBlockState?.amountFieldModel?.fiatValue.isNullOrEmpty()) {
|
||||||
|
_state.update { amountStateFactory.getAmountSecondaryLoadingState() }
|
||||||
|
}
|
||||||
|
fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold(
|
||||||
|
ifLeft = ::handleOnrampError,
|
||||||
|
ifRight = {
|
||||||
|
_state.update {
|
||||||
|
if (!state?.amountBlockState?.amountFieldModel?.fiatValue.isNullOrEmpty()) {
|
||||||
|
return@fold
|
||||||
|
} else {
|
||||||
|
amountStateFactory.getAmountSecondaryResetState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
loadQuotes()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadQuotes() {
|
||||||
|
loadQuotesJob?.cancel()
|
||||||
|
loadQuotesJob = modelScope.launch {
|
||||||
|
runCatching {
|
||||||
|
val content = state.value as? OnrampV2MainComponentUM.Content ?: return@runCatching
|
||||||
|
if (content.amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()) return@runCatching
|
||||||
|
fetchQuotesUseCase.invoke(
|
||||||
|
userWallet = userWallet,
|
||||||
|
amount = content.amountBlockState.amountFieldModel.fiatAmount,
|
||||||
|
cryptoCurrency = params.cryptoCurrency,
|
||||||
|
).onLeft(::handleOnrampError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleOnrampError(onrampError: OnrampError) {
|
||||||
|
Timber.e(onrampError.toString())
|
||||||
|
_state.update { stateFactory.getOnrampErrorState(onrampError) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateProvider(quote: OnrampQuote) {
|
||||||
|
lastUpdateState.value = OnrampLastUpdate(
|
||||||
|
quote.fromAmount,
|
||||||
|
quote.countryCode,
|
||||||
|
)
|
||||||
|
|
||||||
|
_state.update {
|
||||||
|
amountStateFactory.getUpdatedProviderState(selectedQuote = quote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendOnrampQuotesErrorAnalytic(quotes: List<OnrampQuote>) {
|
||||||
|
quotes.forEach { errorState ->
|
||||||
|
when (errorState) {
|
||||||
|
is OnrampQuote.Error -> analyticsEventHandler.sendOnrampErrorEvent(
|
||||||
|
error = errorState.error,
|
||||||
|
tokenSymbol = params.cryptoCurrency.symbol,
|
||||||
|
providerName = errorState.provider.info.name,
|
||||||
|
paymentMethod = errorState.paymentMethod.name,
|
||||||
|
)
|
||||||
|
is OnrampQuote.AmountError -> analyticsEventHandler.sendOnrampErrorEvent(
|
||||||
|
error = errorState.error,
|
||||||
|
tokenSymbol = params.cryptoCurrency.symbol,
|
||||||
|
providerName = errorState.provider.info.name,
|
||||||
|
paymentMethod = errorState.paymentMethod.name,
|
||||||
|
)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkLastInputState(quote: OnrampQuote?): Boolean {
|
||||||
|
return lastUpdateState.value?.lastAmount != quote?.fromAmount ||
|
||||||
|
lastUpdateState.value?.lastCountryString != quote?.countryCode
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendScreenOpenAnalytics() {
|
||||||
|
analyticsEventHandler.send(
|
||||||
|
OnrampAnalyticsEvent.ScreenOpened(
|
||||||
|
source = params.source,
|
||||||
|
tokenSymbol = params.cryptoCurrency.symbol,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.components.Keyboard
|
||||||
|
import com.tangem.core.ui.components.PrimaryButton
|
||||||
|
import com.tangem.core.ui.components.SpacerH
|
||||||
|
import com.tangem.core.ui.components.keyboardAsState
|
||||||
|
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.features.onramp.impl.R
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun OnrampFooterContent(
|
||||||
|
state: OnrampV2MainComponentUM.Content,
|
||||||
|
boxScope: BoxScope,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
boxScope.apply {
|
||||||
|
AnimatedVisibility(
|
||||||
|
modifier = Modifier
|
||||||
|
.imePadding()
|
||||||
|
.align(Alignment.BottomCenter),
|
||||||
|
visible = state.offersBlockState.isBlockVisible.not(),
|
||||||
|
enter = slideInVertically(
|
||||||
|
initialOffsetY = { it },
|
||||||
|
animationSpec = tween(durationMillis = 300),
|
||||||
|
),
|
||||||
|
exit = slideOutVertically(
|
||||||
|
targetOffsetY = { it },
|
||||||
|
animationSpec = tween(durationMillis = 300),
|
||||||
|
),
|
||||||
|
label = "Footer block animation",
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
PrimaryButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
text = stringResourceSafe(id = R.string.common_continue),
|
||||||
|
onClick = {
|
||||||
|
state.continueButtonConfig.onClick()
|
||||||
|
keyboardController?.hide()
|
||||||
|
},
|
||||||
|
enabled = state.continueButtonConfig.enabled,
|
||||||
|
)
|
||||||
|
SpacerH(16.dp)
|
||||||
|
OnrampAmountButtons(state = state.onrampAmountButtonUMState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun OnrampAmountButtons(state: OnrampV2AmountButtonUMState) {
|
||||||
|
val keyboard by keyboardAsState()
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = state is OnrampV2AmountButtonUMState.Loaded,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
) {
|
||||||
|
when (state) {
|
||||||
|
is OnrampV2AmountButtonUMState.Loaded -> {
|
||||||
|
if (keyboard is Keyboard.Opened) {
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier.background(color = TangemTheme.colors.button.secondary),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
vertical = 10.dp,
|
||||||
|
horizontal = 8.dp,
|
||||||
|
),
|
||||||
|
state = rememberLazyListState(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = state.amountButtons,
|
||||||
|
key = OnrampAmountButtonUM::value,
|
||||||
|
) {
|
||||||
|
AmountButton(button = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OnrampV2AmountButtonUMState.None -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AmountButton(button: OnrampAmountButtonUM, modifier: Modifier = Modifier) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.sizeIn(minHeight = 24.dp, minWidth = 62.dp)
|
||||||
|
.background(
|
||||||
|
color = TangemTheme.colors.field.primary,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
)
|
||||||
|
.clickable(onClick = button.onClick)
|
||||||
|
.padding(vertical = 4.dp, horizontal = 20.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${button.value}${button.currency}",
|
||||||
|
style = TangemTheme.typography.caption1,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.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.Scaffold
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.components.RectangleShimmer
|
||||||
|
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||||
|
import com.tangem.core.ui.components.notifications.Notification
|
||||||
|
import com.tangem.core.ui.extensions.resolveReference
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun OnrampNewMainScreen(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier.systemBarsPadding(),
|
||||||
|
topBar = {
|
||||||
|
TangemTopAppBar(
|
||||||
|
startButton = state.topBarConfig.startButtonUM,
|
||||||
|
endButton = state.topBarConfig.endButtonUM,
|
||||||
|
title = state.topBarConfig.title.resolveReference(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentWindowInsets = WindowInsetsZero,
|
||||||
|
containerColor = TangemTheme.colors.background.secondary,
|
||||||
|
) { scaffoldPaddings ->
|
||||||
|
OnrampNewMainComponentContent(
|
||||||
|
state = state,
|
||||||
|
modifier = Modifier.padding(scaffoldPaddings),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun OnrampNewMainComponentContent(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(TangemTheme.colors.background.secondary),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
when (state) {
|
||||||
|
is OnrampV2MainComponentUM.InitialLoading -> InitialLoading(state = state)
|
||||||
|
is OnrampV2MainComponentUM.Content -> Content(state = state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state is OnrampV2MainComponentUM.Content) {
|
||||||
|
OnrampFooterContent(
|
||||||
|
state = state,
|
||||||
|
boxScope = this,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InitialLoading(state: OnrampV2MainComponentUM.InitialLoading, modifier: Modifier = Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||||
|
) {
|
||||||
|
OnrampAmountContentLoading()
|
||||||
|
if (state.errorNotification != null) Notification(config = state.errorNotification.config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun OnrampAmountContentLoading() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
|
||||||
|
.background(TangemTheme.colors.background.action)
|
||||||
|
.padding(vertical = TangemTheme.dimens.spacing28),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = TangemTheme.dimens.spacing16)
|
||||||
|
.size(width = 76.dp, height = 20.dp),
|
||||||
|
radius = TangemTheme.dimens.radius4,
|
||||||
|
)
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = TangemTheme.dimens.spacing12)
|
||||||
|
.size(width = 136.dp, height = 44.dp),
|
||||||
|
radius = TangemTheme.dimens.radius4,
|
||||||
|
)
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = TangemTheme.dimens.spacing8)
|
||||||
|
.size(width = 52.dp, height = 16.dp),
|
||||||
|
radius = TangemTheme.dimens.radius4,
|
||||||
|
)
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = TangemTheme.dimens.spacing20)
|
||||||
|
.size(width = 84.dp, height = 28.dp),
|
||||||
|
radius = TangemTheme.dimens.radius14,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Content(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.padding(
|
||||||
|
bottom = 76.dp,
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||||
|
) {
|
||||||
|
OnrampV2AmountContent(state = state)
|
||||||
|
|
||||||
|
OnrampOffersContent(state = state.offersBlockState)
|
||||||
|
|
||||||
|
if (state.errorNotification != null) Notification(config = state.errorNotification.config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun OnrampOffersContent(state: OnrampOffersBlockUM) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = state.isBlockVisible,
|
||||||
|
enter = slideInVertically(
|
||||||
|
initialOffsetY = { it },
|
||||||
|
animationSpec = tween(durationMillis = 300),
|
||||||
|
),
|
||||||
|
exit = slideOutVertically(
|
||||||
|
targetOffsetY = { it },
|
||||||
|
animationSpec = tween(durationMillis = 300),
|
||||||
|
),
|
||||||
|
label = "Offers block animation",
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Some offers",
|
||||||
|
style = TangemTheme.typography.head,
|
||||||
|
)
|
||||||
|
// TODO in [REDACTED_TASK_KEY] to be continued
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
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.platform.testTag
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||||
|
import com.tangem.core.ui.components.SpacerH
|
||||||
|
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.extensions.stringResourceSafe
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags
|
||||||
|
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||||
|
import com.tangem.features.onramp.impl.R
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampNewAmountSecondaryFieldUM
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampNewCurrencyUM
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||||
|
val padding = remember(state.offersBlockState.isBlockVisible) {
|
||||||
|
if (state.offersBlockState.isBlockVisible) {
|
||||||
|
22.dp
|
||||||
|
} else {
|
||||||
|
46.dp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.background(
|
||||||
|
color = TangemTheme.colors.background.action,
|
||||||
|
shape = RoundedCornerShape(size = TangemTheme.dimens.radius16),
|
||||||
|
)
|
||||||
|
.padding(vertical = padding)
|
||||||
|
.animateContentSize(animationSpec = tween(durationMillis = 300)),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
OnrampHeaderTitle()
|
||||||
|
|
||||||
|
SpacerH(12.dp)
|
||||||
|
|
||||||
|
OnrampAmountField(
|
||||||
|
amountField = state.amountBlockState.amountFieldModel,
|
||||||
|
currencyCode = state.amountBlockState.currencyUM.code,
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerH(8.dp)
|
||||||
|
|
||||||
|
OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel)
|
||||||
|
|
||||||
|
SpacerH(20.dp)
|
||||||
|
|
||||||
|
OnrampCurrencyIcon(currencyUM = state.amountBlockState.currencyUM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun OnrampHeaderTitle() {
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.onramp_you_will_pay_title),
|
||||||
|
style = TangemTheme.typography.subtitle2,
|
||||||
|
color = TangemTheme.colors.text.tertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: String) {
|
||||||
|
val decimalFormat = rememberDecimalFormat()
|
||||||
|
val requester = remember { FocusRequester() }
|
||||||
|
AmountTextField(
|
||||||
|
value = amountField.fiatValue,
|
||||||
|
decimals = amountField.fiatAmount.decimals,
|
||||||
|
visualTransformation = AmountVisualTransformation(
|
||||||
|
decimals = amountField.fiatAmount.decimals,
|
||||||
|
symbol = currencyCode,
|
||||||
|
currencyCode = currencyCode,
|
||||||
|
decimalFormat = decimalFormat,
|
||||||
|
symbolColor = TangemTheme.colors.text.disabled,
|
||||||
|
),
|
||||||
|
onValueChange = amountField.onValueChange,
|
||||||
|
keyboardOptions = amountField.keyboardOptions,
|
||||||
|
keyboardActions = amountField.keyboardActions,
|
||||||
|
textStyle = TangemTheme.typography.head.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)
|
||||||
|
.testTag(BuyTokenDetailsScreenTestTags.FIAT_AMOUNT_TEXT_FIELD),
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = Unit) {
|
||||||
|
requester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun OnrampAmountSecondary(state: OnrampNewAmountSecondaryFieldUM) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
top = TangemTheme.dimens.spacing8,
|
||||||
|
start = TangemTheme.dimens.spacing12,
|
||||||
|
end = TangemTheme.dimens.spacing12,
|
||||||
|
)
|
||||||
|
.testTag(BuyTokenDetailsScreenTestTags.TOKEN_AMOUNT),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
when (state) {
|
||||||
|
is OnrampNewAmountSecondaryFieldUM.Content -> Text(
|
||||||
|
text = state.amount.resolveReference(),
|
||||||
|
style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr),
|
||||||
|
color = TangemTheme.colors.text.tertiary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
is OnrampNewAmountSecondaryFieldUM.Error -> Text(
|
||||||
|
text = state.error.resolveReference(),
|
||||||
|
color = TangemTheme.colors.text.warning,
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
is OnrampNewAmountSecondaryFieldUM.Loading -> TextShimmer(
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
modifier = Modifier.width(TangemTheme.dimens.size62),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun OnrampCurrencyIcon(currencyUM: OnrampNewCurrencyUM, modifier: Modifier = Modifier) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(14.dp))
|
||||||
|
.background(TangemTheme.colors.button.secondary)
|
||||||
|
.clickable(onClick = currencyUM.onClick)
|
||||||
|
.padding(horizontal = 6.dp, vertical = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.testTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON),
|
||||||
|
model = currencyUM.iconUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = currencyUM.code,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(TangemTheme.dimens.size16)
|
||||||
|
.testTag(BuyTokenDetailsScreenTestTags.EXPAND_FIAT_LIST_BUTTON),
|
||||||
|
painter = painterResource(id = R.drawable.ic_chevron_24),
|
||||||
|
tint = TangemTheme.colors.icon.informative,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package com.tangem.features.onramp.newmain
|
|
||||||
|
|
||||||
internal interface OnrampNewMainFeatureToggle {
|
|
||||||
val isOnrampNewMainEnabled: Boolean
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue