Updated on 2026-08-14
This commit is contained in:
parent
52c39047d6
commit
2be3a915ea
51 changed files with 597 additions and 2448 deletions
|
|
@ -146,7 +146,6 @@ abstract class BaseTestCase : TestCase(
|
|||
return ApplicationInjectionExecutionRule(
|
||||
toggleStates = mapOf(
|
||||
"SWAP_REDESIGN_ENABLED" to false,
|
||||
"NEW_ONRAMP_MAIN_ENABLED" to true,
|
||||
"HOT_WALLET_ENABLED" to true,
|
||||
"ACCOUNTS_FEATURE_ENABLED" to true,
|
||||
"FEED_ENABLED" to true,
|
||||
|
|
|
|||
|
|
@ -24,10 +24,6 @@
|
|||
"name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED",
|
||||
"version": "5.32.0"
|
||||
},
|
||||
{
|
||||
"name": "NEW_ONRAMP_MAIN_ENABLED",
|
||||
"version": "5.31.0"
|
||||
},
|
||||
{
|
||||
"name": "ACCOUNTS_FEATURE_ENABLED",
|
||||
"version": "5.33.0"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.onramp.alloffers.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM
|
||||
|
||||
internal interface AllOffersIntents {
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import com.tangem.core.ui.format.bigdecimal.percent
|
|||
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
|
||||
import com.tangem.domain.onramp.model.*
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.common.ui.notifications.NotificationUM
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import com.tangem.domain.onramp.model.PaymentMethodStatus
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal sealed interface AllOffersStateUM {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.tangem.features.onramp.alloffers.entity.AllOffersIntents
|
|||
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
|
||||
import com.tangem.features.onramp.alloffers.entity.AllOffersStateFactory
|
||||
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
|
|||
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
|
||||
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.mainv2.ui.Offer
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.main.ui.Offer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ import com.tangem.domain.onramp.model.PaymentMethodType
|
|||
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
|
||||
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.mainv2.ui.TimingBlock
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOfferUM
|
||||
import com.tangem.features.onramp.main.ui.TimingBlock
|
||||
import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
|
|||
|
|
@ -8,15 +8,16 @@ 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.arkivanov.essenty.lifecycle.subscribe
|
||||
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.alloffers.AllOffersComponent
|
||||
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.providers.SelectProviderComponent
|
||||
import com.tangem.features.onramp.main.ui.OnrampMainScreen
|
||||
import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -27,10 +28,15 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
|
|||
@Assisted private val params: OnrampMainComponent.Params,
|
||||
private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory,
|
||||
private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory,
|
||||
private val selectProviderComponentFactory: SelectProviderComponent.Factory,
|
||||
private val allOffersComponentFactory: AllOffersComponent.Factory,
|
||||
) : OnrampMainComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: OnrampMainComponentModel = getOrCreateModel(params)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(onStop = model::onStop)
|
||||
}
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = null,
|
||||
|
|
@ -43,7 +49,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
|
|||
val state by model.state.collectAsState()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
OnrampMainComponentContent(modifier = modifier, state = state)
|
||||
OnrampMainScreen(modifier = modifier, state = state)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +63,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
|
|||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
country = config.country,
|
||||
isLaunchSepa = params.isLaunchSepa,
|
||||
isLaunchSepa = false,
|
||||
onDismiss = {
|
||||
model.bottomSheetNavigation.dismiss()
|
||||
model.handleOnrampAvailable()
|
||||
|
|
@ -72,14 +78,14 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
|
|||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
is OnrampMainBottomSheetConfig.ProvidersList -> selectProviderComponentFactory.create(
|
||||
is OnrampMainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = SelectProviderComponent.Params(
|
||||
onProviderClick = model::onProviderSelected,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
selectedProviderId = config.selectedProviderId,
|
||||
selectedPaymentMethod = config.selectedPaymentMethod,
|
||||
params = AllOffersComponent.Params(
|
||||
userWallet = model.userWallet,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
openRedirectPage = params.openRedirectPage,
|
||||
amountCurrencyCode = config.amountCurrencyCode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ internal interface OnrampMainComponent : ComposableContentComponent {
|
|||
val source: OnrampSource,
|
||||
val openSettings: () -> Unit,
|
||||
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
|
||||
val isLaunchSepa: Boolean,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnrampMainComponent>
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.onramp.main.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.onramp.main.model.OnrampMainComponentModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface OnrampMainComponentModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnrampMainComponentModel::class)
|
||||
fun bindOnrampSelectCountryModel(model: OnrampMainComponentModel): Model
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
package com.tangem.features.onramp.main.di
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.onramp.main.DefaultOnrampMainComponent
|
||||
import com.tangem.features.onramp.main.OnrampMainComponent
|
||||
import com.tangem.features.onramp.main.model.OnrampMainComponentModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -15,4 +19,9 @@ internal interface OnrampMainComponentModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindOnrampMainComponentFactory(factory: DefaultOnrampMainComponent.Factory): OnrampMainComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnrampMainComponentModel::class)
|
||||
fun bindOnrampMainComponentModel(model: OnrampMainComponentModel): Model
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
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
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class OnrampNewAmountBlockUM(
|
||||
val currencyUM: OnrampNewCurrencyUM,
|
||||
internal data class OnrampAmountBlockUM(
|
||||
val currencyUM: OnrampCurrencyUM,
|
||||
val amountFieldModel: AmountFieldModel,
|
||||
val secondaryFieldModel: OnrampSecondaryFieldErrorUM,
|
||||
)
|
||||
|
||||
internal data class OnrampNewCurrencyUM(
|
||||
internal data class OnrampCurrencyUM(
|
||||
val unit: String,
|
||||
val code: String,
|
||||
val iconUrl: String?,
|
||||
|
|
@ -25,9 +25,9 @@ internal sealed interface OnrampSecondaryFieldErrorUM {
|
|||
data class Error(val error: TextReference) : OnrampSecondaryFieldErrorUM
|
||||
}
|
||||
|
||||
internal sealed interface OnrampV2AmountButtonUMState {
|
||||
data class Loaded(val amountButtons: ImmutableList<OnrampAmountButtonUM>) : OnrampV2AmountButtonUMState
|
||||
data object None : OnrampV2AmountButtonUMState
|
||||
internal sealed interface OnrampAmountButtonUMState {
|
||||
data class Loaded(val amountButtons: ImmutableList<OnrampAmountButtonUM>) : OnrampAmountButtonUMState
|
||||
data object None : OnrampAmountButtonUMState
|
||||
}
|
||||
|
||||
internal data class OnrampAmountButtonUM(
|
||||
|
|
@ -2,12 +2,15 @@ package com.tangem.features.onramp.main.entity
|
|||
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
|
||||
interface OnrampIntents {
|
||||
fun onAmountValueChanged(value: String, isValuePasted: Boolean)
|
||||
internal interface OnrampIntents {
|
||||
fun onAmountValueChanged(value: String)
|
||||
fun openSettings()
|
||||
fun openCurrenciesList()
|
||||
fun onBuyClick(quote: OnrampProviderWithQuote.Data)
|
||||
fun onBuyClick(
|
||||
quote: OnrampProviderWithQuote.Data,
|
||||
onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM,
|
||||
categoryUM: OnrampOfferCategoryUM,
|
||||
)
|
||||
fun openProviders()
|
||||
fun onRefresh()
|
||||
fun onLinkClick(link: String)
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampAmount
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
|
||||
data class OnrampLastUpdate(
|
||||
val fromAmount: OnrampAmount,
|
||||
val countryCode: String,
|
||||
val paymentMethod: OnrampPaymentMethod,
|
||||
)
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampCountry
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal sealed interface OnrampMainBottomSheetConfig {
|
||||
sealed interface OnrampMainBottomSheetConfig {
|
||||
@Serializable
|
||||
data class ConfirmResidency(val country: OnrampCountry) : OnrampMainBottomSheetConfig
|
||||
|
||||
|
|
@ -13,8 +12,5 @@ internal sealed interface OnrampMainBottomSheetConfig {
|
|||
data object CurrenciesList : OnrampMainBottomSheetConfig
|
||||
|
||||
@Serializable
|
||||
data class ProvidersList(
|
||||
val selectedProviderId: String,
|
||||
val selectedPaymentMethod: OnrampPaymentMethod,
|
||||
) : OnrampMainBottomSheetConfig
|
||||
data class AllOffers(val amountCurrencyCode: String) : OnrampMainBottomSheetConfig
|
||||
}
|
||||
|
|
@ -4,55 +4,29 @@ 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
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.onramp.impl.R
|
||||
|
||||
@Immutable
|
||||
internal sealed interface OnrampMainComponentUM {
|
||||
|
||||
val topBarConfig: OnrampMainTopBarUM
|
||||
val buyButtonConfig: BuyButtonConfig
|
||||
val errorNotification: NotificationUM?
|
||||
|
||||
data class InitialLoading(
|
||||
val currency: String,
|
||||
val onClose: () -> Unit,
|
||||
val openSettings: () -> Unit,
|
||||
override val errorNotification: NotificationUM? = null,
|
||||
) : OnrampMainComponentUM {
|
||||
override val topBarConfig: OnrampMainTopBarUM = OnrampMainTopBarUM(
|
||||
title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")),
|
||||
startButtonUM = TopAppBarButtonUM.Back(
|
||||
onBackClicked = onClose,
|
||||
enabled = true,
|
||||
),
|
||||
endButtonUM = TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_more_vertical_24,
|
||||
onClicked = openSettings,
|
||||
isEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
override val buyButtonConfig: BuyButtonConfig = BuyButtonConfig(
|
||||
text = resourceReference(R.string.common_buy),
|
||||
onClick = {},
|
||||
isEnabled = false,
|
||||
)
|
||||
}
|
||||
override val topBarConfig: OnrampMainTopBarUM,
|
||||
override val errorNotification: NotificationUM?,
|
||||
) : OnrampMainComponentUM
|
||||
|
||||
data class Content(
|
||||
override val topBarConfig: OnrampMainTopBarUM,
|
||||
override val buyButtonConfig: BuyButtonConfig,
|
||||
override val errorNotification: NotificationUM?,
|
||||
val amountBlockState: OnrampAmountBlockUM,
|
||||
val providerBlockState: OnrampProviderBlockUM,
|
||||
val offersBlockState: OnrampOffersBlockUM,
|
||||
val onrampAmountButtonUMState: OnrampAmountButtonUMState,
|
||||
) : OnrampMainComponentUM
|
||||
}
|
||||
|
||||
internal data class BuyButtonConfig(
|
||||
val text: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
val isEnabled: Boolean,
|
||||
internal data class OnrampMainTopBarUM(
|
||||
val title: TextReference,
|
||||
val startButtonUM: TopAppBarButtonUM,
|
||||
val endButtonUM: TopAppBarButtonUM,
|
||||
)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class OnrampMainTopBarUM(
|
||||
val title: TextReference,
|
||||
val startButtonUM: TopAppBarButtonUM,
|
||||
val endButtonUM: TopAppBarButtonUM,
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
|
||||
sealed class OnrampProviderBlockUM {
|
||||
data object Empty : OnrampProviderBlockUM()
|
||||
data object Loading : OnrampProviderBlockUM()
|
||||
data class Content(
|
||||
val providerId: String,
|
||||
val paymentMethod: OnrampPaymentMethod,
|
||||
val providerName: String,
|
||||
val termsOfUseLink: String?,
|
||||
val privacyPolicyLink: String?,
|
||||
val isBestRate: Boolean,
|
||||
val onLinkClick: (String) -> Unit,
|
||||
val onClick: () -> Unit,
|
||||
) : OnrampProviderBlockUM()
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.onramp.main.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
|
||||
sealed interface OnrampProvidersUM {
|
||||
|
||||
data object Empty : OnrampProvidersUM
|
||||
|
||||
data object Loading : OnrampProvidersUM
|
||||
|
||||
data class Content(
|
||||
val providerId: String,
|
||||
val paymentMethod: OnrampPaymentMethod,
|
||||
) : OnrampProvidersUM
|
||||
}
|
||||
|
|
@ -1,24 +1,24 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.converter
|
||||
package com.tangem.features.onramp.main.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.utils.parseBigDecimalOrNull
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory
|
||||
import com.tangem.features.onramp.main.entity.*
|
||||
import com.tangem.features.onramp.main.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>,
|
||||
internal class OnrampAmountFieldChangeConverter(
|
||||
private val currentStateProvider: Provider<OnrampMainComponentUM>,
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||
private val onrampIntents: OnrampV2Intents,
|
||||
) : Converter<String, OnrampV2MainComponentUM> {
|
||||
private val onrampIntents: OnrampIntents,
|
||||
) : Converter<String, OnrampMainComponentUM> {
|
||||
|
||||
override fun convert(value: String): OnrampV2MainComponentUM {
|
||||
override fun convert(value: String): OnrampMainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
if (state !is OnrampV2MainComponentUM.Content) return state
|
||||
if (state !is OnrampMainComponentUM.Content) return state
|
||||
|
||||
if (value.isEmpty()) return state.emptyState()
|
||||
|
||||
|
|
@ -36,13 +36,13 @@ internal class OnrampV2AmountFieldChangeConverter(
|
|||
amountFieldModel = amountFieldModel,
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
onrampAmountButtonUMState = OnrampAmountButtonUMState.None,
|
||||
offersBlockState = OnrampOffersBlockUM.Loading,
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun OnrampV2MainComponentUM.Content.emptyState(): OnrampV2MainComponentUM.Content {
|
||||
private fun OnrampMainComponentUM.Content.emptyState(): OnrampMainComponentUM.Content {
|
||||
val amountFieldModel = amountBlockState.amountFieldModel.copy(
|
||||
value = "",
|
||||
fiatValue = "",
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.factory
|
||||
package com.tangem.features.onramp.main.entity.factory
|
||||
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState
|
||||
import com.tangem.features.onramp.main.entity.OnrampAmountButtonUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampAmountButtonUMState
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class OnrampAmountButtonUMStateFactory {
|
||||
|
|
@ -12,7 +12,7 @@ internal class OnrampAmountButtonUMStateFactory {
|
|||
currencyCode: String,
|
||||
currencySymbol: String,
|
||||
onAmountValueChanged: (String) -> Unit,
|
||||
): OnrampV2AmountButtonUMState {
|
||||
): OnrampAmountButtonUMState {
|
||||
return when (currencyCode) {
|
||||
USD_CODE, EUR_CODE -> {
|
||||
val buttons = defaultPreselectedAmount.map { value ->
|
||||
|
|
@ -22,9 +22,9 @@ internal class OnrampAmountButtonUMStateFactory {
|
|||
onClick = { onAmountValueChanged(value.toString()) },
|
||||
)
|
||||
}.toPersistentList()
|
||||
OnrampV2AmountButtonUMState.Loaded(buttons)
|
||||
OnrampAmountButtonUMState.Loaded(buttons)
|
||||
}
|
||||
else -> OnrampV2AmountButtonUMState.None
|
||||
else -> OnrampAmountButtonUMState.None
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.factory
|
||||
package com.tangem.features.onramp.main.entity.factory
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
|
@ -11,34 +11,34 @@ 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.mainv2.entity.*
|
||||
import com.tangem.features.onramp.mainv2.entity.converter.OnrampV2AmountFieldChangeConverter
|
||||
import com.tangem.features.onramp.main.entity.*
|
||||
import com.tangem.features.onramp.main.entity.converter.OnrampAmountFieldChangeConverter
|
||||
import com.tangem.utils.Provider
|
||||
|
||||
internal class OnrampV2AmountStateFactory(
|
||||
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||
internal class OnrampAmountStateFactory(
|
||||
private val currentStateProvider: Provider<OnrampMainComponentUM>,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val onrampIntents: OnrampV2Intents,
|
||||
private val onrampIntents: OnrampIntents,
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||
) {
|
||||
|
||||
private val onrampAmountFieldChangeConverter: OnrampV2AmountFieldChangeConverter by lazy(
|
||||
private val onrampAmountFieldChangeConverter: OnrampAmountFieldChangeConverter by lazy(
|
||||
mode = LazyThreadSafetyMode.NONE,
|
||||
) {
|
||||
OnrampV2AmountFieldChangeConverter(
|
||||
OnrampAmountFieldChangeConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
onrampIntents = onrampIntents,
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnAmountValueChange(value: String): OnrampV2MainComponentUM {
|
||||
fun getOnAmountValueChange(value: String): OnrampMainComponentUM {
|
||||
return onrampAmountFieldChangeConverter.convert(value)
|
||||
}
|
||||
|
||||
fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampV2MainComponentUM {
|
||||
fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
|
||||
|
|
@ -72,9 +72,9 @@ internal class OnrampV2AmountStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getSecondaryFieldAmountErrorState(quotes: List<OnrampQuote>): OnrampV2MainComponentUM {
|
||||
fun getSecondaryFieldAmountErrorState(quotes: List<OnrampQuote>): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
if (amountState.amountFieldModel.fiatValue.isEmpty()) return currentState
|
||||
|
|
@ -91,23 +91,23 @@ internal class OnrampV2AmountStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryFieldResetState(): OnrampV2MainComponentUM {
|
||||
fun getAmountSecondaryFieldResetState(): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
if (amountState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Empty) return currentState
|
||||
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
onrampAmountButtonUMState = OnrampAmountButtonUMState.None,
|
||||
errorNotification = null,
|
||||
offersBlockState = currentState.offersBlockState,
|
||||
)
|
||||
}
|
||||
|
||||
private fun OnrampQuote.AmountError.toSecondaryFieldUiModel(
|
||||
amountState: OnrampNewAmountBlockUM,
|
||||
amountState: OnrampAmountBlockUM,
|
||||
): OnrampSecondaryFieldErrorUM.Error {
|
||||
val amount = error.requiredAmount.format {
|
||||
fiat(
|
||||
|
|
@ -1,24 +1,23 @@
|
|||
package com.tangem.features.onramp.mainv2.entity.factory
|
||||
package com.tangem.features.onramp.main.entity.factory
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.onramp.model.*
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.main.entity.*
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class OnrampOffersStateFactory(
|
||||
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||
private val onrampIntents: OnrampV2Intents,
|
||||
private val currentStateProvider: Provider<OnrampMainComponentUM>,
|
||||
private val onrampIntents: OnrampIntents,
|
||||
) {
|
||||
|
||||
fun getOffersState(offers: List<OnrampOffersBlock>): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider.invoke()
|
||||
return when (currentState) {
|
||||
is OnrampV2MainComponentUM.InitialLoading -> currentState
|
||||
is OnrampV2MainComponentUM.Content -> {
|
||||
fun getOffersState(offers: List<OnrampOffersBlock>): OnrampMainComponentUM {
|
||||
return when (val currentState = currentStateProvider.invoke()) {
|
||||
is OnrampMainComponentUM.InitialLoading -> currentState
|
||||
is OnrampMainComponentUM.Content -> {
|
||||
if (currentState.offersBlockState is OnrampOffersBlockUM.Loading) {
|
||||
return currentState
|
||||
}
|
||||
|
|
@ -8,7 +8,9 @@ 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.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
|
|
@ -22,16 +24,30 @@ import java.math.BigDecimal
|
|||
|
||||
internal class OnrampStateFactory(
|
||||
private val currentStateProvider: Provider<OnrampMainComponentUM>,
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
private val onrampIntents: OnrampIntents,
|
||||
) {
|
||||
|
||||
fun getInitialState(currency: String, onClose: () -> Unit): OnrampMainComponentUM.InitialLoading {
|
||||
fun getInitialState(
|
||||
currency: String,
|
||||
onClose: () -> Unit,
|
||||
openSettings: () -> Unit,
|
||||
): OnrampMainComponentUM.InitialLoading {
|
||||
return OnrampMainComponentUM.InitialLoading(
|
||||
currency = currency,
|
||||
onClose = onClose,
|
||||
openSettings = onrampIntents::openSettings,
|
||||
errorNotification = null,
|
||||
topBarConfig = OnrampMainTopBarUM(
|
||||
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,
|
||||
isEnabled = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -42,12 +58,19 @@ internal class OnrampStateFactory(
|
|||
is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true)
|
||||
is TopAppBarButtonUM.Text -> button.copy(isEnabled = true)
|
||||
}
|
||||
|
||||
val initialAmountBlockState = getInitialAmountBlockState(currency)
|
||||
|
||||
return OnrampMainComponentUM.Content(
|
||||
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||
buyButtonConfig = state.buyButtonConfig,
|
||||
amountBlockState = getInitialAmountBlockState(currency),
|
||||
providerBlockState = OnrampProviderBlockUM.Empty,
|
||||
amountBlockState = initialAmountBlockState,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
errorNotification = null,
|
||||
onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton(
|
||||
currencyCode = currency.code,
|
||||
currencySymbol = currency.unit,
|
||||
onAmountValueChanged = onrampIntents::onAmountValueChanged,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -68,21 +91,6 @@ internal class OnrampStateFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getNoPairsErrorState(): OnrampMainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val contentState = state as? OnrampMainComponentUM.Content ?: return state
|
||||
|
||||
return contentState.copy(
|
||||
buyButtonConfig = contentState.buyButtonConfig.copy(isEnabled = false),
|
||||
amountBlockState = contentState.amountBlockState.copy(
|
||||
amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true),
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Error(
|
||||
error = resourceReference(R.string.onramp_no_available_providers),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampMainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val endButton = when (val button = state.topBarConfig.endButtonUM) {
|
||||
|
|
@ -93,15 +101,15 @@ internal class OnrampStateFactory(
|
|||
return when (state) {
|
||||
is OnrampMainComponentUM.Content -> state.copy(
|
||||
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||
buyButtonConfig = state.buyButtonConfig.copy(isEnabled = false),
|
||||
amountBlockState = state.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY),
|
||||
),
|
||||
providerBlockState = OnrampProviderBlockUM.Empty,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
errorCode = errorCode,
|
||||
onRefresh = onRefresh,
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampAmountButtonUMState.None,
|
||||
amountBlockState = state.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
)
|
||||
is OnrampMainComponentUM.InitialLoading -> state.copy(
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
|
|
@ -112,6 +120,22 @@ internal class OnrampStateFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getNoPairsErrorState(): OnrampMainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val contentState = state as? OnrampMainComponentUM.Content ?: return state
|
||||
|
||||
return contentState.copy(
|
||||
amountBlockState = contentState.amountBlockState.copy(
|
||||
amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true),
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Error(
|
||||
error = resourceReference(R.string.onramp_no_available_providers),
|
||||
),
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampAmountButtonUMState.None,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampAmountBlockUM {
|
||||
return OnrampAmountBlockUM(
|
||||
currencyUM = OnrampCurrencyUM(
|
||||
|
|
@ -119,11 +143,12 @@ internal class OnrampStateFactory(
|
|||
iconUrl = currency.image,
|
||||
precision = currency.precision,
|
||||
onClick = onrampIntents::openCurrenciesList,
|
||||
unit = currency.unit,
|
||||
),
|
||||
amountFieldModel = AmountFieldModel(
|
||||
value = "",
|
||||
fiatValue = "",
|
||||
onValueChange = { onrampIntents.onAmountValueChanged(value = it, isValuePasted = false) },
|
||||
onValueChange = onrampIntents::onAmountValueChanged,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.None,
|
||||
keyboardType = KeyboardType.Number,
|
||||
|
|
@ -139,7 +164,7 @@ internal class OnrampStateFactory(
|
|||
isValuePasted = false,
|
||||
onValuePastedTriggerDismiss = {},
|
||||
),
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY),
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -149,8 +174,4 @@ internal class OnrampStateFactory(
|
|||
decimals = currency.precision,
|
||||
type = AmountType.FiatType(currency.code),
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val PREDEFINED_SEPA_AMOUNT = "100"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
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.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimalOrNull
|
||||
import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM
|
||||
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<OnrampAmountFieldChangeConverter.Input, OnrampMainComponentUM> {
|
||||
|
||||
override fun convert(input: Input): OnrampMainComponentUM {
|
||||
val value = input.value
|
||||
val isValuePasted = input.isValuePasted
|
||||
|
||||
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.parseBigDecimalOrNull() ?: 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,
|
||||
),
|
||||
isValuePasted = isValuePasted,
|
||||
)
|
||||
|
||||
return state.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
amountFieldModel = amountFieldModel,
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading,
|
||||
),
|
||||
providerBlockState = OnrampProviderBlockUM.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.Content(TextReference.EMPTY),
|
||||
),
|
||||
buyButtonConfig = buyButtonConfig.copy(isEnabled = false),
|
||||
providerBlockState = OnrampProviderBlockUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
data class Input(val value: String, val isValuePasted: Boolean)
|
||||
}
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
package com.tangem.features.onramp.main.entity.factory.amount
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
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.OnrampProviderWithQuote
|
||||
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.*
|
||||
import com.tangem.features.onramp.providers.entity.SelectProviderResult
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.extensions.isSingleItem
|
||||
|
||||
internal class OnrampAmountStateFactory(
|
||||
private val currentStateProvider: Provider<OnrampMainComponentUM>,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val onrampIntents: OnrampIntents,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
private val needApplyFCARestrictions: Provider<Boolean>,
|
||||
) {
|
||||
|
||||
private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
)
|
||||
|
||||
fun getOnAmountValueChange(value: String, isValuePasted: Boolean) =
|
||||
onrampAmountFieldChangeConverter.convert(OnrampAmountFieldChangeConverter.Input(value, isValuePasted))
|
||||
|
||||
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(
|
||||
isError = false,
|
||||
fiatAmount = amountState.amountFieldModel.fiatAmount.copy(
|
||||
currencySymbol = currency.unit,
|
||||
decimals = currency.precision,
|
||||
type = AmountType.FiatType(currency.code),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryLoadingState(): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading),
|
||||
providerBlockState = OnrampProviderBlockUM.Loading,
|
||||
buyButtonConfig = currentState.buyButtonConfig.copy(isEnabled = false),
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampMainComponentUM.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,
|
||||
),
|
||||
buyButtonConfig = currentState.buyButtonConfig.copy(
|
||||
isEnabled = quote is OnrampQuote.Data,
|
||||
onClick = {
|
||||
if (quote is OnrampQuote.Data) {
|
||||
onrampIntents.onBuyClick(
|
||||
OnrampProviderWithQuote.Data(
|
||||
provider = quote.provider,
|
||||
paymentMethod = quote.paymentMethod,
|
||||
toAmount = quote.toAmount,
|
||||
fromAmount = quote.fromAmount,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun getUpdatedProviderState(selectedQuote: OnrampQuote, quotes: List<OnrampQuote>): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.ProviderCalculated(
|
||||
providerName = selectedQuote.provider.info.name,
|
||||
tokenSymbol = cryptoCurrency.symbol,
|
||||
paymentMethod = selectedQuote.paymentMethod.name,
|
||||
),
|
||||
)
|
||||
|
||||
val bestProvider = quotes.firstOrNull()
|
||||
val isMultipleQuotes = !quotes.isSingleItem()
|
||||
val isOtherQuotesHasData = quotes
|
||||
.filter { it.paymentMethod == selectedQuote.paymentMethod }
|
||||
.filterNot { it == bestProvider }
|
||||
.any { it is OnrampQuote.Data }
|
||||
|
||||
val isBestProvider = selectedQuote == bestProvider &&
|
||||
isMultipleQuotes &&
|
||||
isOtherQuotesHasData &&
|
||||
!needApplyFCARestrictions()
|
||||
|
||||
return currentState.copy(
|
||||
providerBlockState = selectedQuote.toProviderBlockState(isBestProvider),
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryUpdatedState(
|
||||
providerResult: SelectProviderResult,
|
||||
isBestRate: Boolean,
|
||||
): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
val secondaryField = when (providerResult) {
|
||||
is SelectProviderResult.ProviderWithError -> {
|
||||
providerResult.quoteError.toSecondaryFieldUiModel(amountState)
|
||||
}
|
||||
is SelectProviderResult.ProviderWithQuote -> {
|
||||
val amount = providerResult.toAmount.value.format {
|
||||
crypto(symbol = providerResult.toAmount.symbol, decimals = providerResult.toAmount.decimals)
|
||||
}
|
||||
OnrampAmountSecondaryFieldUM.Content(stringReference(amount))
|
||||
}
|
||||
}
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(secondaryFieldModel = secondaryField),
|
||||
providerBlockState = OnrampProviderBlockUM.Content(
|
||||
paymentMethod = providerResult.paymentMethod,
|
||||
providerId = providerResult.provider.id,
|
||||
providerName = providerResult.provider.info.name,
|
||||
isBestRate = isBestRate && !needApplyFCARestrictions(),
|
||||
onClick = onrampIntents::openProviders,
|
||||
termsOfUseLink = providerResult.provider.info.termsOfUseLink,
|
||||
privacyPolicyLink = providerResult.provider.info.privacyPolicyLink,
|
||||
onLinkClick = onrampIntents::onLinkClick,
|
||||
),
|
||||
buyButtonConfig = currentState.buyButtonConfig.copy(
|
||||
isEnabled = providerResult is SelectProviderResult.ProviderWithQuote,
|
||||
onClick = {
|
||||
if (providerResult is SelectProviderResult.ProviderWithQuote) {
|
||||
onrampIntents.onBuyClick(
|
||||
OnrampProviderWithQuote.Data(
|
||||
provider = providerResult.provider,
|
||||
paymentMethod = providerResult.paymentMethod,
|
||||
toAmount = providerResult.toAmount,
|
||||
fromAmount = providerResult.fromAmount,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryResetState(): OnrampMainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampMainComponentUM.Content) return currentState
|
||||
|
||||
val amountState = currentState.amountBlockState
|
||||
|
||||
if (amountState.secondaryFieldModel is OnrampAmountSecondaryFieldUM.Content) return currentState
|
||||
|
||||
return currentState.copy(
|
||||
amountBlockState = amountState.copy(
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(
|
||||
amount = TextReference.EMPTY,
|
||||
),
|
||||
),
|
||||
errorNotification = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun OnrampQuote.toProviderBlockState(isBestRate: Boolean): OnrampProviderBlockUM {
|
||||
return OnrampProviderBlockUM.Content(
|
||||
paymentMethod = paymentMethod,
|
||||
providerId = provider.id,
|
||||
providerName = provider.info.name,
|
||||
isBestRate = isBestRate,
|
||||
onClick = onrampIntents::openProviders,
|
||||
termsOfUseLink = provider.info.termsOfUseLink,
|
||||
privacyPolicyLink = provider.info.privacyPolicyLink,
|
||||
onLinkClick = onrampIntents::onLinkClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM? {
|
||||
return when (this) {
|
||||
is OnrampQuote.Error -> null
|
||||
is OnrampQuote.Data -> {
|
||||
val amount = toAmount.value.format {
|
||||
crypto(symbol = toAmount.symbol, decimals = toAmount.decimals)
|
||||
}
|
||||
OnrampAmountSecondaryFieldUM.Content(stringReference(amount))
|
||||
}
|
||||
is OnrampQuote.AmountError -> this.toSecondaryFieldUiModel(amountState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun OnrampQuote.AmountError.toSecondaryFieldUiModel(
|
||||
amountState: OnrampAmountBlockUM,
|
||||
): OnrampAmountSecondaryFieldUM.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 OnrampAmountSecondaryFieldUM.Error(
|
||||
resourceReference(
|
||||
errorTextRes,
|
||||
wrappedList(amount),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +1,25 @@
|
|||
package com.tangem.features.onramp.main.model
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
|
||||
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.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.components.fields.InputManager
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
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.OnrampCurrency
|
||||
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.settings.usercountry.GetUserCountryUseCase
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onramp.main.OnrampMainComponent
|
||||
import com.tangem.features.onramp.main.entity.*
|
||||
import com.tangem.features.onramp.main.entity.factory.OnrampAmountButtonUMStateFactory
|
||||
import com.tangem.features.onramp.main.entity.factory.OnrampAmountStateFactory
|
||||
import com.tangem.features.onramp.main.entity.factory.OnrampOffersStateFactory
|
||||
import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory
|
||||
import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory.Companion.PREDEFINED_SEPA_AMOUNT
|
||||
import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory
|
||||
import com.tangem.features.onramp.providers.entity.SelectProviderResult
|
||||
import com.tangem.features.onramp.utils.model.EUR_CURRENCY
|
||||
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -43,7 +30,6 @@ import com.tangem.utils.isNullOrZero
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
|
|
@ -51,85 +37,141 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val router: Router,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
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 onrampSaveDefaultCurrencyUseCase: OnrampSaveDefaultCurrencyUseCase,
|
||||
private val onrampGetDefaultCurrencyUseCase: OnrampGetDefaultCurrencyUseCase,
|
||||
private val amountInputManager: InputManager,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val urlOpener: UrlOpener,
|
||||
getWalletsUseCase: GetWalletsUseCase,
|
||||
getUserCountryUseCase: GetUserCountryUseCase,
|
||||
private val getOnrampOffersUseCase: GetOnrampOffersUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
getWalletsUseCase: GetWalletsUseCase,
|
||||
) : Model(), OnrampIntents {
|
||||
|
||||
private val params: OnrampMainComponent.Params = paramsContainer.require()
|
||||
val params = paramsContainer.require<OnrampMainComponent.Params>()
|
||||
|
||||
private var shouldForceChooseSepa = params.isLaunchSepa
|
||||
private var currencyToRestore: OnrampCurrency? = null
|
||||
|
||||
val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<OnrampMainBottomSheetConfig> = SlotNavigation()
|
||||
|
||||
private val lastUpdateState = mutableStateOf<OnrampLastUpdate?>(null)
|
||||
private var userCountry: UserCountry? = null
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampAmountButtonUMStateFactory()
|
||||
}
|
||||
|
||||
@Suppress("PropertyUsedBeforeDeclaration")
|
||||
private val stateFactory = OnrampStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onrampIntents = this,
|
||||
)
|
||||
private val stateFactory: OnrampStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onrampIntents = this,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
}
|
||||
|
||||
val state: StateFlow<OnrampMainComponentUM>
|
||||
field = MutableStateFlow<OnrampMainComponentUM>(
|
||||
value = stateFactory.getInitialState(
|
||||
currency = params.cryptoCurrency.name,
|
||||
onClose = ::onCloseClick,
|
||||
openSettings = ::openSettings,
|
||||
),
|
||||
)
|
||||
|
||||
private val amountStateFactory = OnrampAmountStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
onrampIntents = this,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
needApplyFCARestrictions = Provider { userCountry.needApplyFCARestrictions() },
|
||||
)
|
||||
private val amountStateFactory: OnrampAmountStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampAmountStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
onrampIntents = this,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
}
|
||||
|
||||
private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampOffersStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
onrampIntents = this,
|
||||
)
|
||||
}
|
||||
|
||||
private val quotesTaskScheduler = SingleTaskScheduler<Unit>()
|
||||
|
||||
init {
|
||||
userCountry = getUserCountryUseCase.invokeSync().getOrNull()
|
||||
?: UserCountry.Other(Locale.getDefault().country)
|
||||
val bottomSheetNavigation: SlotNavigation<OnrampMainBottomSheetConfig> = SlotNavigation()
|
||||
val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
clearOnrampCacheUseCase()
|
||||
|
||||
if (params.isLaunchSepa) {
|
||||
currencyToRestore = onrampGetDefaultCurrencyUseCase.invoke().getOrNull()
|
||||
onrampSaveDefaultCurrencyUseCase.invoke(EUR_CURRENCY)
|
||||
}
|
||||
}
|
||||
|
||||
startLoadingQuotes()
|
||||
sendScreenOpenAnalytics()
|
||||
checkResidenceCountry()
|
||||
subscribeToAmountChanges()
|
||||
subscribeToCountryAndCurrencyUpdates()
|
||||
subscribeToQuotesUpdate()
|
||||
subscribeOnOffers()
|
||||
}
|
||||
|
||||
private fun sendScreenOpenAnalytics() {
|
||||
override fun onDestroy() {
|
||||
modelScope.launch { clearOnrampCacheUseCase.invoke() }
|
||||
quotesTaskScheduler.cancelTask()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onAmountValueChanged(value: String) {
|
||||
state.update { amountStateFactory.getOnAmountValueChange(value) }
|
||||
modelScope.launch { amountInputManager.update(value) }
|
||||
}
|
||||
|
||||
override fun openSettings() {
|
||||
params.openSettings.invoke()
|
||||
}
|
||||
|
||||
override fun openCurrenciesList() {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened())
|
||||
bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList)
|
||||
}
|
||||
|
||||
override fun onBuyClick(
|
||||
quote: OnrampProviderWithQuote.Data,
|
||||
onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM,
|
||||
categoryUM: OnrampOfferCategoryUM,
|
||||
) {
|
||||
val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.ScreenOpened(
|
||||
source = params.source,
|
||||
OnrampAnalyticsEvent.OnBuyClick(
|
||||
providerName = quote.provider.info.name,
|
||||
currency = currentContentState.amountBlockState.currencyUM.code,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
sendOfferClickEvent(
|
||||
quote = quote,
|
||||
onrampOfferAdvantagesUM = onrampOfferAdvantagesUM,
|
||||
categoryUM = categoryUM,
|
||||
)
|
||||
params.openRedirectPage(quote)
|
||||
}
|
||||
|
||||
override fun openProviders() {
|
||||
val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return
|
||||
val amountCurrentCode = currentContentState.amountBlockState.currencyUM.code
|
||||
bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.AllOffers(amountCurrentCode))
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
state.update {
|
||||
stateFactory.getInitialState(
|
||||
currency = params.cryptoCurrency.name,
|
||||
onClose = router::pop,
|
||||
openSettings = ::openSettings,
|
||||
)
|
||||
}
|
||||
modelScope.launch {
|
||||
clearOnrampCacheUseCase.invoke()
|
||||
checkResidenceCountry()
|
||||
handleOnrampAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
quotesTaskScheduler.cancelTask()
|
||||
}
|
||||
|
||||
fun handleOnrampAvailable() {
|
||||
|
|
@ -137,92 +179,6 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
subscribeToQuotesUpdate()
|
||||
}
|
||||
|
||||
fun onProviderSelected(result: SelectProviderResult, isBestRate: Boolean) {
|
||||
state.update { amountStateFactory.getAmountSecondaryUpdatedState(result, isBestRate) }
|
||||
|
||||
if (result.paymentMethod.id != SEPA_METHOD_ID) {
|
||||
shouldForceChooseSepa = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkResidenceCountry() {
|
||||
modelScope.launch {
|
||||
checkOnrampAvailabilityUseCase(userWallet)
|
||||
.onRight(::handleOnrampAvailability)
|
||||
.onLeft(::handleOnrampError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnrampAvailability(availability: OnrampAvailability) {
|
||||
when (availability) {
|
||||
is OnrampAvailability.Available -> handleOnrampAvailable()
|
||||
is OnrampAvailability.ConfirmResidency,
|
||||
is OnrampAvailability.NotSupported,
|
||||
-> bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country))
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToCountryAndCurrencyUpdates() {
|
||||
getOnrampCountryUseCase.invoke()
|
||||
.onEach { maybeCountry ->
|
||||
maybeCountry.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { country ->
|
||||
if (country == null) return@onEach
|
||||
|
||||
val wasInitialLoading = state.value is OnrampMainComponentUM.InitialLoading
|
||||
state.update { prevState ->
|
||||
if (prevState is OnrampMainComponentUM.InitialLoading) {
|
||||
stateFactory.getReadyState(country.defaultCurrency)
|
||||
} else {
|
||||
amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
updatePairsAndQuotes()
|
||||
|
||||
if (wasInitialLoading && params.isLaunchSepa) {
|
||||
onAmountValueChanged(value = PREDEFINED_SEPA_AMOUNT, isValuePasted = true)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToAmountChanges() = modelScope.launch {
|
||||
amountInputManager.query
|
||||
.filter(String::isNotEmpty)
|
||||
.collectLatest { _ ->
|
||||
state.update { amountStateFactory.getAmountSecondaryLoadingState() }
|
||||
startLoadingQuotes()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updatePairsAndQuotes() {
|
||||
state.update { prevState ->
|
||||
val contentState = state.value as? OnrampMainComponentUM.Content ?: return@update prevState
|
||||
|
||||
if (contentState.amountBlockState.amountFieldModel.fiatValue.isNotEmpty()) {
|
||||
amountStateFactory.getAmountSecondaryLoadingState()
|
||||
} else {
|
||||
prevState
|
||||
}
|
||||
}
|
||||
|
||||
fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { state.update { amountStateFactory.getAmountSecondaryResetState() } },
|
||||
)
|
||||
startLoadingQuotes()
|
||||
}
|
||||
|
||||
private fun handleOnrampError(onrampError: OnrampError) {
|
||||
Timber.e(onrampError.toString())
|
||||
sendOnrampErrorAnalytic(onrampError)
|
||||
state.update { stateFactory.getOnrampErrorState(onrampError) }
|
||||
}
|
||||
|
||||
private fun startLoadingQuotes() {
|
||||
quotesTaskScheduler.cancelTask()
|
||||
quotesTaskScheduler.scheduleTask(scope = modelScope, task = loadQuotesTask())
|
||||
|
|
@ -233,11 +189,12 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
delay = UPDATE_DELAY,
|
||||
task = {
|
||||
runSuspendCatching {
|
||||
val content = state.value as? OnrampMainComponentUM.Content ?: return@runSuspendCatching
|
||||
val amountBlockState = content.amountBlockState
|
||||
if (amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()) {
|
||||
return@runSuspendCatching
|
||||
}
|
||||
val amountBlockState = (state.value as? OnrampMainComponentUM.Content)?.amountBlockState
|
||||
?: return@runSuspendCatching
|
||||
|
||||
val fiatAmount = amountBlockState.amountFieldModel.fiatAmount
|
||||
if (fiatAmount.value.isNullOrZero()) return@runSuspendCatching
|
||||
|
||||
fetchQuotesUseCase.invoke(
|
||||
userWallet = userWallet,
|
||||
amount = amountBlockState.amountFieldModel.fiatAmount,
|
||||
|
|
@ -250,6 +207,84 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
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(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country))
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCloseClick() {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp())
|
||||
router.pop()
|
||||
}
|
||||
|
||||
private fun subscribeOnOffers() = modelScope.launch {
|
||||
getOnrampOffersUseCase
|
||||
.invoke()
|
||||
.collectLatest { maybeOffers ->
|
||||
maybeOffers.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { offers ->
|
||||
val currentState = state.value
|
||||
if (currentState is OnrampMainComponentUM.Content) {
|
||||
if (currentState.amountBlockState.amountFieldModel.fiatValue.isEmpty()) {
|
||||
state.update {
|
||||
currentState.copy(offersBlockState = OnrampOffersBlockUM.Empty)
|
||||
}
|
||||
return@fold
|
||||
}
|
||||
state.update {
|
||||
onrampOffersStateFactory.getOffersState(offers)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToAmountChanges() = modelScope.launch {
|
||||
amountInputManager.query
|
||||
.filter(String::isNotEmpty)
|
||||
.collectLatest { _ ->
|
||||
startLoadingQuotes()
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToCountryAndCurrencyUpdates() {
|
||||
getOnrampCountryUseCase.invoke()
|
||||
.onEach { maybeCountry ->
|
||||
maybeCountry.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { country ->
|
||||
if (country == null) return@onEach
|
||||
state.update { prevState ->
|
||||
when (prevState) {
|
||||
is OnrampMainComponentUM.Content -> {
|
||||
amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency)
|
||||
}
|
||||
is OnrampMainComponentUM.InitialLoading -> {
|
||||
stateFactory.getReadyState(country.defaultCurrency)
|
||||
}
|
||||
}
|
||||
}
|
||||
updatePairsAndQuotes()
|
||||
},
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToQuotesUpdate() {
|
||||
getOnrampQuotesUseCase.invoke()
|
||||
.conflate()
|
||||
|
|
@ -262,152 +297,31 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
override fun onAmountValueChanged(value: String, isValuePasted: Boolean) {
|
||||
state.update { amountStateFactory.getOnAmountValueChange(value, isValuePasted) }
|
||||
modelScope.launch { amountInputManager.update(value) }
|
||||
}
|
||||
|
||||
override fun openSettings() {
|
||||
params.openSettings()
|
||||
}
|
||||
|
||||
override fun onBuyClick(quote: OnrampProviderWithQuote.Data) {
|
||||
if (userWallet is UserWallet.Cold && isDemoCardUseCase.invoke(userWallet.cardId)) {
|
||||
showDemoWarning()
|
||||
} else {
|
||||
val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.OnBuyClick(
|
||||
providerName = quote.provider.info.name,
|
||||
currency = currentContentState.amountBlockState.currencyUM.code,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
params.openRedirectPage(quote)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openCurrenciesList() {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened())
|
||||
bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList)
|
||||
}
|
||||
|
||||
override fun openProviders() {
|
||||
val providerState = (state.value as? OnrampMainComponentUM.Content)?.providerBlockState ?: return
|
||||
val providerContentState = providerState as? OnrampProviderBlockUM.Content ?: return
|
||||
bottomSheetNavigation.activate(
|
||||
OnrampMainBottomSheetConfig.ProvidersList(
|
||||
selectedPaymentMethod = providerContentState.paymentMethod,
|
||||
selectedProviderId = providerContentState.providerId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
state.update {
|
||||
stateFactory.getInitialState(
|
||||
currency = params.cryptoCurrency.name,
|
||||
onClose = router::pop,
|
||||
)
|
||||
}
|
||||
quotesTaskScheduler.cancelTask()
|
||||
modelScope.launch {
|
||||
clearOnrampCacheUseCase.invoke()
|
||||
checkResidenceCountry()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLinkClick(link: String) = urlOpener.openUrl(link)
|
||||
|
||||
override fun onDestroy() {
|
||||
modelScope.launch { clearOnrampCacheUseCase.invoke() }
|
||||
quotesTaskScheduler.cancelTask()
|
||||
|
||||
modelScope.launch {
|
||||
if (params.isLaunchSepa) {
|
||||
currencyToRestore?.let { onrampSaveDefaultCurrencyUseCase.invoke(it) }
|
||||
}
|
||||
}
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun onCloseClick() {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp())
|
||||
router.pop()
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
|
||||
/**
|
||||
* !!! Important quote selection logic !!!
|
||||
* Selects or updated quote based on input data (amount, country, currency).
|
||||
* If input data has changed select new best quote, otherwise last selected quote.
|
||||
* If last selected quote on same input data is in an error state, select next best quote
|
||||
* If new best quote or next best quote does not exist (i.e. Error state) select nothing.
|
||||
*/
|
||||
private fun selectOrUpdateQuote(quotes: List<OnrampQuote>): OnrampQuote? {
|
||||
val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error }
|
||||
|
||||
val bestSepaQuote = if (params.isLaunchSepa && shouldForceChooseSepa) {
|
||||
quotes.filterIsInstance<OnrampQuote.Data>()
|
||||
.filter { it.paymentMethod.id == SEPA_METHOD_ID }
|
||||
.maxByOrNull { it.toAmount.value }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Check if amount, country or currency has changed
|
||||
val newQuote = bestSepaQuote ?: if (isAmountOrCountryChanged(quoteToCheck)) {
|
||||
quoteToCheck
|
||||
} else {
|
||||
val state = state.value as? OnrampMainComponentUM.Content
|
||||
val providerState = state?.providerBlockState as? OnrampProviderBlockUM.Content
|
||||
|
||||
// Get current selected quote to update
|
||||
val lastSelectedQuote = quotes.firstOrNull { quote ->
|
||||
quote.provider.id == providerState?.providerId &&
|
||||
quote.paymentMethod.id == providerState.paymentMethod.id
|
||||
when {
|
||||
quotes.isEmpty() -> {
|
||||
state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
}
|
||||
|
||||
// Check if selected updated quote is not error
|
||||
if (lastSelectedQuote is OnrampQuote.Error) {
|
||||
quoteToCheck
|
||||
} else {
|
||||
lastSelectedQuote
|
||||
quotes.all { it is OnrampQuote.AmountError } -> {
|
||||
state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) }
|
||||
}
|
||||
quotes.none { it is OnrampQuote.Data } -> {
|
||||
state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
}
|
||||
else -> {
|
||||
state.update { prevState ->
|
||||
val resetState = amountStateFactory.getAmountSecondaryFieldResetState()
|
||||
if (prevState is OnrampMainComponentUM.Content &&
|
||||
resetState is OnrampMainComponentUM.Content &&
|
||||
prevState.offersBlockState is OnrampOffersBlockUM.Loading
|
||||
) {
|
||||
resetState.copy(offersBlockState = OnrampOffersBlockUM.Empty)
|
||||
} else {
|
||||
resetState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newQuote != null) {
|
||||
updateProvider(newQuote, quotes)
|
||||
}
|
||||
|
||||
return newQuote
|
||||
}
|
||||
|
||||
private fun updateProvider(quote: OnrampQuote, quotes: List<OnrampQuote>) {
|
||||
lastUpdateState.value = OnrampLastUpdate(
|
||||
quote.fromAmount,
|
||||
quote.countryCode,
|
||||
quote.paymentMethod,
|
||||
)
|
||||
|
||||
if (quote.paymentMethod.id != SEPA_METHOD_ID) {
|
||||
shouldForceChooseSepa = false
|
||||
}
|
||||
|
||||
state.update {
|
||||
amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -415,41 +329,30 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
state.update { prevState ->
|
||||
(prevState as? OnrampMainComponentUM.Content)?.copy(
|
||||
errorNotification = null,
|
||||
providerBlockState = OnrampProviderBlockUM.Loading,
|
||||
offersBlockState = OnrampOffersBlockUM.Loading,
|
||||
amountBlockState = prevState.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading,
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
) ?: prevState
|
||||
}
|
||||
startLoadingQuotes()
|
||||
}
|
||||
|
||||
private fun showDemoWarning() {
|
||||
val alertUM = AlertDemoModeUM(onConfirmClick = {})
|
||||
val message = DialogMessage(
|
||||
title = alertUM.title,
|
||||
message = alertUM.message,
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = alertUM.confirmButtonText,
|
||||
onClick = alertUM.onConfirmClick,
|
||||
)
|
||||
private suspend fun updatePairsAndQuotes() {
|
||||
fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = {
|
||||
state.update {
|
||||
amountStateFactory.getAmountSecondaryFieldResetState()
|
||||
}
|
||||
startLoadingQuotes()
|
||||
},
|
||||
secondActionBuilder = { cancelAction() },
|
||||
)
|
||||
|
||||
messageSender.send(message)
|
||||
}
|
||||
|
||||
private fun sendOnrampErrorAnalytic(error: OnrampError) {
|
||||
val content = state.value as? OnrampMainComponentUM.Content
|
||||
val providerContent = content?.providerBlockState as? OnrampProviderBlockUM.Content
|
||||
analyticsEventHandler.sendOnrampErrorEvent(
|
||||
error = error,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
providerName = providerContent?.providerName,
|
||||
paymentMethod = providerContent?.paymentMethod?.name,
|
||||
)
|
||||
private fun handleOnrampError(onrampError: OnrampError) {
|
||||
Timber.e(onrampError.toString())
|
||||
state.update { stateFactory.getOnrampErrorState(onrampError) }
|
||||
}
|
||||
|
||||
private fun sendOnrampQuotesErrorAnalytic(quotes: List<OnrampQuote>) {
|
||||
|
|
@ -467,20 +370,48 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
providerName = errorState.provider.info.name,
|
||||
paymentMethod = errorState.paymentMethod.name,
|
||||
)
|
||||
else -> { /* no-op */
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAmountOrCountryChanged(quote: OnrampQuote?): Boolean {
|
||||
return lastUpdateState.value?.fromAmount != quote?.fromAmount ||
|
||||
lastUpdateState.value?.countryCode != quote?.countryCode
|
||||
private fun sendScreenOpenAnalytics() {
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.ScreenOpened(
|
||||
source = params.source,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendOfferClickEvent(
|
||||
quote: OnrampProviderWithQuote.Data,
|
||||
onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM,
|
||||
categoryUM: OnrampOfferCategoryUM,
|
||||
) {
|
||||
val event = when (categoryUM) {
|
||||
OnrampOfferCategoryUM.RecentlyUsed -> {
|
||||
OnrampAnalyticsEvent.RecentlyBuyClicked(
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
providerName = quote.provider.info.name,
|
||||
paymentMethod = quote.paymentMethod.name,
|
||||
)
|
||||
}
|
||||
OnrampOfferCategoryUM.Recommended -> {
|
||||
onrampOfferAdvantagesUM.toAnalyticsEvent(
|
||||
cryptoCurrencySymbol = params.cryptoCurrency.symbol,
|
||||
providerName = quote.provider.info.name,
|
||||
paymentMethodName = quote.paymentMethod.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (event != null) {
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val UPDATE_DELAY = 10_000L
|
||||
|
||||
const val SEPA_METHOD_ID = "sepa"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.features.onramp.main.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -17,61 +19,89 @@ 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.TextShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
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.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.features.onramp.main.entity.OnrampSecondaryFieldErrorUM
|
||||
|
||||
@Composable
|
||||
internal fun OnrampAmountContent(state: OnrampAmountBlockUM, modifier: Modifier = Modifier) {
|
||||
internal fun OnrampAmountContent(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(vertical = TangemTheme.dimens.spacing28),
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = RoundedCornerShape(size = TangemTheme.dimens.radius16),
|
||||
)
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp)
|
||||
.animateContentSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
OnrampCurrencyIcon(currencyUM = state.currencyUM)
|
||||
OnrampAmountField(amountField = state.amountFieldModel)
|
||||
OnrampAmountSecondary(state = state.secondaryFieldModel)
|
||||
OnrampHeaderTitle()
|
||||
|
||||
OnrampAmountField(
|
||||
amountField = state.amountBlockState.amountFieldModel,
|
||||
currencyCode = state.amountBlockState.currencyUM.code,
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state.amountBlockState.secondaryFieldModel !is OnrampSecondaryFieldErrorUM.Empty,
|
||||
) {
|
||||
if (state.amountBlockState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Error) {
|
||||
OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel)
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
OnrampCurrencyIcon(currencyUM = state.amountBlockState.currencyUM)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountField(amountField: AmountFieldModel) {
|
||||
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() }
|
||||
val symbolColor = if (amountField.fiatValue.isBlank()) {
|
||||
TangemTheme.colors.text.disabled
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
}
|
||||
AmountTextField(
|
||||
value = amountField.fiatValue,
|
||||
decimals = amountField.fiatAmount.decimals,
|
||||
visualTransformation = AmountVisualTransformation(
|
||||
decimals = amountField.fiatAmount.decimals,
|
||||
symbol = amountField.fiatAmount.currencySymbol,
|
||||
currencyCode = amountField.fiatAmount.currencySymbol,
|
||||
symbol = currencyCode,
|
||||
currencyCode = currencyCode,
|
||||
decimalFormat = decimalFormat,
|
||||
symbolColor = symbolColor,
|
||||
symbolColor = if (amountField.fiatValue.isBlank()) {
|
||||
TangemTheme.colors.text.disabled
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
),
|
||||
onValueChange = amountField.onValueChange,
|
||||
keyboardOptions = amountField.keyboardOptions,
|
||||
keyboardActions = amountField.keyboardActions,
|
||||
textStyle = TangemTheme.typography.h2.copy(
|
||||
textStyle = TangemTheme.typography.head.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
|
|
@ -82,7 +112,8 @@ private fun OnrampAmountField(amountField: AmountFieldModel) {
|
|||
modifier = Modifier
|
||||
.focusRequester(requester)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing24,
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
bottom = TangemTheme.dimens.spacing4,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
|
|
@ -96,7 +127,7 @@ private fun OnrampAmountField(amountField: AmountFieldModel) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) {
|
||||
private fun OnrampAmountSecondary(state: OnrampSecondaryFieldErrorUM.Error) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -107,24 +138,12 @@ private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) {
|
|||
),
|
||||
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),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = state.error.resolveReference(),
|
||||
color = TangemTheme.colors.text.warning,
|
||||
style = TangemTheme.typography.caption2,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,20 +151,27 @@ private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) {
|
|||
private fun OnrampCurrencyIcon(currencyUM: OnrampCurrencyUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable(onClick = currencyUM.onClick)
|
||||
.padding(start = TangemTheme.dimens.spacing24),
|
||||
.padding(horizontal = 6.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size40)
|
||||
.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)
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
package com.tangem.features.onramp.main.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.extensions.appendColored
|
||||
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.main.entity.OnrampMainComponentUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM
|
||||
|
||||
private const val TERMS_OF_USE_KEY = "termsOfUse"
|
||||
private const val PRIVACY_POLICY_KEY = "privacyPolicy"
|
||||
|
||||
@Composable
|
||||
internal fun OnrampButtonComponent(state: OnrampMainComponentUM) {
|
||||
val content = state as? OnrampMainComponentUM.Content
|
||||
val providerState = content?.providerBlockState as? OnrampProviderBlockUM.Content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
OnrampTosText(providerState)
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(id = R.string.common_buy),
|
||||
onClick = state.buyButtonConfig.onClick,
|
||||
enabled = state.buyButtonConfig.isEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampTosText(provider: OnrampProviderBlockUM.Content?) {
|
||||
val termsOfUse = stringResourceSafe(R.string.common_terms_of_use)
|
||||
val privacyPolicy = stringResourceSafe(R.string.common_privacy_policy)
|
||||
val tosText = stringResourceSafe(R.string.onramp_legal, termsOfUse, privacyPolicy)
|
||||
|
||||
val clickableAnnotation = buildAnnotatedString {
|
||||
append(tosText.substringBefore(termsOfUse))
|
||||
|
||||
pushStringAnnotation(TERMS_OF_USE_KEY, "")
|
||||
appendColored(termsOfUse, TangemTheme.colors.text.accent)
|
||||
pop()
|
||||
|
||||
append(tosText.substringAfter(termsOfUse).substringBefore(privacyPolicy))
|
||||
|
||||
pushStringAnnotation(PRIVACY_POLICY_KEY, "")
|
||||
appendColored(privacyPolicy, TangemTheme.colors.text.accent)
|
||||
pop()
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
targetState = provider,
|
||||
transitionSpec = { fadeIn().togetherWith(fadeOut()) },
|
||||
label = "Onramp Legal Info Animation",
|
||||
) { state ->
|
||||
val termsOfUseLink = provider?.termsOfUseLink
|
||||
val privacyPolicyLink = provider?.privacyPolicyLink
|
||||
|
||||
if (state != null && termsOfUseLink != null && privacyPolicyLink != null) {
|
||||
ClickableText(
|
||||
text = clickableAnnotation,
|
||||
style = TangemTheme.typography.caption2.copy(
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
onClick = { offset ->
|
||||
val tosAnnotations = clickableAnnotation.getStringAnnotations(
|
||||
tag = TERMS_OF_USE_KEY,
|
||||
start = offset,
|
||||
end = offset,
|
||||
)
|
||||
|
||||
if (tosAnnotations.any()) {
|
||||
state.onLinkClick(termsOfUseLink)
|
||||
}
|
||||
|
||||
val privacyPolicyAnnotations = clickableAnnotation.getStringAnnotations(
|
||||
tag = PRIVACY_POLICY_KEY,
|
||||
start = offset,
|
||||
end = offset,
|
||||
)
|
||||
|
||||
if (privacyPolicyAnnotations.any()) {
|
||||
state.onLinkClick(privacyPolicyLink)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.mainv2.ui
|
||||
package com.tangem.features.onramp.main.ui
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
|
|
@ -20,13 +20,13 @@ import com.tangem.core.ui.components.Keyboard
|
|||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampAmountButtonUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampAmountButtonUMState
|
||||
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
|
||||
import com.tangem.features.onramp.main.entity.OnrampOffersBlockUM
|
||||
|
||||
@Composable
|
||||
internal fun BoxScope.OnrampFooterContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
internal fun BoxScope.OnrampFooterContent(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(
|
||||
modifier = modifier
|
||||
.imePadding()
|
||||
|
|
@ -53,16 +53,16 @@ internal fun BoxScope.OnrampFooterContent(state: OnrampV2MainComponentUM.Content
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountButtons(state: OnrampV2AmountButtonUMState) {
|
||||
private fun OnrampAmountButtons(state: OnrampAmountButtonUMState) {
|
||||
val keyboard by keyboardAsState()
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state is OnrampV2AmountButtonUMState.Loaded,
|
||||
visible = state is OnrampAmountButtonUMState.Loaded,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
when (state) {
|
||||
is OnrampV2AmountButtonUMState.Loaded -> {
|
||||
is OnrampAmountButtonUMState.Loaded -> {
|
||||
if (keyboard is Keyboard.Opened) {
|
||||
LazyRow(
|
||||
modifier = Modifier.background(color = TangemTheme.colors.button.secondary),
|
||||
|
|
@ -82,7 +82,7 @@ private fun OnrampAmountButtons(state: OnrampV2AmountButtonUMState) {
|
|||
}
|
||||
}
|
||||
}
|
||||
OnrampV2AmountButtonUMState.None -> Unit
|
||||
OnrampAmountButtonUMState.None -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,13 +5,12 @@ 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
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
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
|
||||
|
|
@ -21,41 +20,58 @@ import com.tangem.core.ui.utils.WindowInsetsZero
|
|||
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
|
||||
|
||||
@Composable
|
||||
internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: Modifier = Modifier) {
|
||||
internal fun OnrampMainScreen(state: OnrampMainComponentUM, modifier: Modifier = Modifier) {
|
||||
Scaffold(
|
||||
modifier = modifier.imePadding(),
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
modifier = modifier.systemBarsPadding(),
|
||||
topBar = {
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
startButton = state.topBarConfig.startButtonUM,
|
||||
endButton = state.topBarConfig.endButtonUM,
|
||||
title = state.topBarConfig.title.resolveReference(),
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
val contentModifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
) { scaffoldPaddings ->
|
||||
OnrampMainComponentContent(
|
||||
state = state,
|
||||
modifier = Modifier.padding(scaffoldPaddings),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.secondary),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
when (state) {
|
||||
is OnrampMainComponentUM.InitialLoading -> InitialLoading(modifier = contentModifier, state = state)
|
||||
is OnrampMainComponentUM.Content -> Content(modifier = contentModifier, state = state)
|
||||
is OnrampMainComponentUM.InitialLoading -> InitialLoading(state = state)
|
||||
is OnrampMainComponentUM.Content -> Content(state = state)
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
OnrampButtonComponent(state)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
)
|
||||
}
|
||||
|
||||
if (state is OnrampMainComponentUM.Content) {
|
||||
OnrampFooterContent(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
OnrampAmountContentLoading()
|
||||
|
|
@ -64,27 +80,38 @@ private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampAmountContentLoading(modifier: Modifier = Modifier) {
|
||||
private fun OnrampAmountContentLoading() {
|
||||
Column(
|
||||
modifier = modifier
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(vertical = TangemTheme.dimens.spacing28),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size40))
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing16)
|
||||
.size(width = TangemTheme.dimens.size96, height = TangemTheme.dimens.size24),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
.size(width = 76.dp, height = 20.dp),
|
||||
radius = TangemTheme.dimens.radius4,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing16)
|
||||
.size(width = TangemTheme.dimens.size72, height = TangemTheme.dimens.size12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -93,13 +120,20 @@ private fun OnrampAmountContentLoading(modifier: Modifier = Modifier) {
|
|||
private fun Content(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = TangemTheme.dimens.spacing76),
|
||||
.padding(
|
||||
bottom = 76.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
OnrampAmountContent(state = state.amountBlockState)
|
||||
OnrampProviderContent(state = state.providerBlockState, modifier = Modifier.fillMaxWidth())
|
||||
OnrampAmountContent(state = state)
|
||||
|
||||
OnrampOffersContent(state = state.offersBlockState)
|
||||
|
||||
if (state.errorNotification != null) Notification(config = state.errorNotification.config)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onramp.mainv2.ui
|
||||
package com.tangem.features.onramp.main.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
|
|
@ -33,7 +33,7 @@ import com.tangem.core.ui.test.OnrampOffersBlockTestTags
|
|||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import com.tangem.domain.onramp.model.PaymentMethodType
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.mainv2.entity.*
|
||||
import com.tangem.features.onramp.main.entity.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
package com.tangem.features.onramp.main.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
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.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.tangem.core.ui.extensions.appendSpace
|
||||
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.main.entity.OnrampProviderBlockUM
|
||||
import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon
|
||||
|
||||
@Composable
|
||||
internal fun OnrampProviderContent(state: OnrampProviderBlockUM, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is OnrampProviderBlockUM.Empty -> Unit
|
||||
is OnrampProviderBlockUM.Loading -> OnrampProviderLoading(modifier)
|
||||
is OnrampProviderBlockUM.Content -> OnrampProviderBlock(modifier = modifier, state = state)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(onClick = state.onClick)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
PaymentMethodIcon(imageUrl = state.paymentMethod.imageUrl)
|
||||
Column(modifier = Modifier.weight(1F)) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append(stringResourceSafe(id = R.string.onramp_pay_with))
|
||||
appendSpace()
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
fontWeight = TangemTheme.typography.subtitle2.fontWeight,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
),
|
||||
) {
|
||||
append(state.paymentMethod.name)
|
||||
}
|
||||
},
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append(stringResourceSafe(id = R.string.onramp_via))
|
||||
appendSpace()
|
||||
append(state.providerName)
|
||||
},
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = state.isBestRate,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
label = "Best Rate visibility animation",
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius4),
|
||||
)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing6,
|
||||
vertical = TangemTheme.dimens.spacing1,
|
||||
),
|
||||
text = stringResourceSafe(id = R.string.express_provider_best_rate),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnrampProviderLoading(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.express_provider),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size16),
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.express_fetch_best_rates),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
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.arkivanov.essenty.lifecycle.subscribe
|
||||
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.alloffers.AllOffersComponent
|
||||
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,
|
||||
private val allOffersComponentFactory: AllOffersComponent.Factory,
|
||||
) : OnrampV2MainComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: OnrampV2MainComponentModel = getOrCreateModel(params)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(onStop = model::onStop)
|
||||
}
|
||||
|
||||
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,
|
||||
isLaunchSepa = false,
|
||||
onDismiss = {
|
||||
model.bottomSheetNavigation.dismiss()
|
||||
model.handleOnrampAvailable()
|
||||
},
|
||||
),
|
||||
)
|
||||
is OnrampV2MainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = SelectCurrencyComponent.Params(
|
||||
userWallet = model.userWallet,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
is OnrampV2MainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AllOffersComponent.Params(
|
||||
userWallet = model.userWallet,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
openRedirectPage = params.openRedirectPage,
|
||||
amountCurrencyCode = config.amountCurrencyCode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : OnrampV2MainComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: OnrampV2MainComponent.Params,
|
||||
): DefaultOnrampV2MainComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
|
||||
class DefaultOnrampV2MainFeatureToggle(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : OnrampV2MainFeatureToggle {
|
||||
override val isOnrampNewMainEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_MAIN_ENABLED")
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
|
||||
internal interface OnrampV2MainComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val source: OnrampSource,
|
||||
val openSettings: () -> Unit,
|
||||
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnrampV2MainComponent>
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2
|
||||
|
||||
internal interface OnrampV2MainFeatureToggle {
|
||||
val isOnrampNewMainEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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 bindOnrampV2MainComponentModel(model: OnrampV2MainComponentModel): Model
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2.entity
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
|
||||
internal interface OnrampV2Intents {
|
||||
fun onAmountValueChanged(value: String)
|
||||
fun openSettings()
|
||||
fun openCurrenciesList()
|
||||
fun onBuyClick(
|
||||
quote: OnrampProviderWithQuote.Data,
|
||||
onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM,
|
||||
categoryUM: OnrampOfferCategoryUM,
|
||||
)
|
||||
fun openProviders()
|
||||
fun onRefresh()
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
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
|
||||
|
||||
@Serializable
|
||||
data class AllOffers(val amountCurrencyCode: String) : OnrampV2MainBottomSheetConfig
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
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 errorNotification: NotificationUM?
|
||||
|
||||
data class InitialLoading(
|
||||
override val topBarConfig: OnrampV2MainTopBarUM,
|
||||
override val errorNotification: NotificationUM?,
|
||||
) : OnrampV2MainComponentUM
|
||||
|
||||
data class Content(
|
||||
override val topBarConfig: OnrampV2MainTopBarUM,
|
||||
override val errorNotification: NotificationUM?,
|
||||
val amountBlockState: OnrampNewAmountBlockUM,
|
||||
val offersBlockState: OnrampOffersBlockUM,
|
||||
val onrampAmountButtonUMState: OnrampV2AmountButtonUMState,
|
||||
) : OnrampV2MainComponentUM
|
||||
}
|
||||
|
||||
internal data class OnrampV2MainTopBarUM(
|
||||
val title: TextReference,
|
||||
val startButtonUM: TopAppBarButtonUM,
|
||||
val endButtonUM: TopAppBarButtonUM,
|
||||
)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
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.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.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: OnrampV2Intents,
|
||||
) {
|
||||
|
||||
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,
|
||||
isEnabled = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getReadyState(currency: OnrampCurrency): OnrampV2MainComponentUM.Content {
|
||||
val state = currentStateProvider()
|
||||
|
||||
val endButton = when (val button = state.topBarConfig.endButtonUM) {
|
||||
is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true)
|
||||
is TopAppBarButtonUM.Text -> button.copy(isEnabled = true)
|
||||
}
|
||||
|
||||
val initialAmountBlockState = getInitialAmountBlockState(currency)
|
||||
|
||||
return OnrampV2MainComponentUM.Content(
|
||||
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||
amountBlockState = initialAmountBlockState,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
errorNotification = null,
|
||||
onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton(
|
||||
currencyCode = currency.code,
|
||||
currencySymbol = currency.unit,
|
||||
onAmountValueChanged = onrampIntents::onAmountValueChanged,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
OnrampError.AlreadyHandledTransaction,
|
||||
-> currentStateProvider() // ignore error state
|
||||
}
|
||||
}
|
||||
|
||||
fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampV2MainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val endButton = when (val button = state.topBarConfig.endButtonUM) {
|
||||
is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true)
|
||||
is TopAppBarButtonUM.Text -> button.copy(isEnabled = true)
|
||||
}
|
||||
|
||||
return when (state) {
|
||||
is OnrampV2MainComponentUM.Content -> state.copy(
|
||||
topBarConfig = state.topBarConfig.copy(endButtonUM = endButton),
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
errorCode = errorCode,
|
||||
onRefresh = onRefresh,
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
amountBlockState = state.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
)
|
||||
is OnrampV2MainComponentUM.InitialLoading -> state.copy(
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
errorCode = errorCode,
|
||||
onRefresh = onRefresh,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNoPairsErrorState(): OnrampV2MainComponentUM {
|
||||
val state = currentStateProvider()
|
||||
val contentState = state as? OnrampV2MainComponentUM.Content ?: return state
|
||||
|
||||
return contentState.copy(
|
||||
amountBlockState = contentState.amountBlockState.copy(
|
||||
amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true),
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Error(
|
||||
error = resourceReference(R.string.onramp_no_available_providers),
|
||||
),
|
||||
),
|
||||
onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
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 = OnrampSecondaryFieldErrorUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal.convertToFiatAmount(currency: OnrampCurrency): Amount = Amount(
|
||||
currencySymbol = currency.unit,
|
||||
value = this,
|
||||
decimals = currency.precision,
|
||||
type = AmountType.FiatType(currency.code),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,417 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2.model
|
||||
|
||||
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.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.OnrampOffersStateFactory
|
||||
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.coroutines.PeriodicTask
|
||||
import com.tangem.utils.coroutines.SingleTaskScheduler
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
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,
|
||||
private val getOnrampOffersUseCase: GetOnrampOffersUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
getWalletsUseCase: GetWalletsUseCase,
|
||||
) : Model(), OnrampV2Intents {
|
||||
|
||||
val params = paramsContainer.require<OnrampV2MainComponent.Params>()
|
||||
|
||||
private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampAmountButtonUMStateFactory()
|
||||
}
|
||||
|
||||
@Suppress("PropertyUsedBeforeDeclaration")
|
||||
private val stateFactory: OnrampV2StateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampV2StateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
onrampIntents = this,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
}
|
||||
|
||||
val state: StateFlow<OnrampV2MainComponentUM>
|
||||
field = MutableStateFlow<OnrampV2MainComponentUM>(
|
||||
value = stateFactory.getInitialState(
|
||||
currency = params.cryptoCurrency.name,
|
||||
onClose = ::onCloseClick,
|
||||
openSettings = ::openSettings,
|
||||
),
|
||||
)
|
||||
|
||||
private val amountStateFactory: OnrampV2AmountStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampV2AmountStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
onrampIntents = this,
|
||||
onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory,
|
||||
)
|
||||
}
|
||||
|
||||
private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
OnrampOffersStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
onrampIntents = this,
|
||||
)
|
||||
}
|
||||
|
||||
private val quotesTaskScheduler = SingleTaskScheduler<Unit>()
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<OnrampV2MainBottomSheetConfig> = SlotNavigation()
|
||||
val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
clearOnrampCacheUseCase()
|
||||
}
|
||||
startLoadingQuotes()
|
||||
sendScreenOpenAnalytics()
|
||||
checkResidenceCountry()
|
||||
subscribeToAmountChanges()
|
||||
subscribeToCountryAndCurrencyUpdates()
|
||||
subscribeToQuotesUpdate()
|
||||
subscribeOnOffers()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
modelScope.launch { clearOnrampCacheUseCase.invoke() }
|
||||
quotesTaskScheduler.cancelTask()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onAmountValueChanged(value: String) {
|
||||
state.update { amountStateFactory.getOnAmountValueChange(value) }
|
||||
modelScope.launch { amountInputManager.update(value) }
|
||||
}
|
||||
|
||||
override fun openSettings() {
|
||||
params.openSettings.invoke()
|
||||
}
|
||||
|
||||
override fun openCurrenciesList() {
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened())
|
||||
bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.CurrenciesList)
|
||||
}
|
||||
|
||||
override fun onBuyClick(
|
||||
quote: OnrampProviderWithQuote.Data,
|
||||
onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM,
|
||||
categoryUM: OnrampOfferCategoryUM,
|
||||
) {
|
||||
val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.OnBuyClick(
|
||||
providerName = quote.provider.info.name,
|
||||
currency = currentContentState.amountBlockState.currencyUM.code,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
sendOfferClickEvent(
|
||||
quote = quote,
|
||||
onrampOfferAdvantagesUM = onrampOfferAdvantagesUM,
|
||||
categoryUM = categoryUM,
|
||||
)
|
||||
params.openRedirectPage(quote)
|
||||
}
|
||||
|
||||
override fun openProviders() {
|
||||
val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return
|
||||
val amountCurrentCode = currentContentState.amountBlockState.currencyUM.code
|
||||
bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.AllOffers(amountCurrentCode))
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
state.update {
|
||||
stateFactory.getInitialState(
|
||||
currency = params.cryptoCurrency.name,
|
||||
onClose = router::pop,
|
||||
openSettings = ::openSettings,
|
||||
)
|
||||
}
|
||||
modelScope.launch {
|
||||
clearOnrampCacheUseCase.invoke()
|
||||
checkResidenceCountry()
|
||||
handleOnrampAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
quotesTaskScheduler.cancelTask()
|
||||
}
|
||||
|
||||
fun handleOnrampAvailable() {
|
||||
subscribeToCountryAndCurrencyUpdates()
|
||||
subscribeToQuotesUpdate()
|
||||
}
|
||||
|
||||
private fun startLoadingQuotes() {
|
||||
quotesTaskScheduler.cancelTask()
|
||||
quotesTaskScheduler.scheduleTask(scope = modelScope, task = loadQuotesTask())
|
||||
}
|
||||
|
||||
private fun loadQuotesTask(): PeriodicTask<Unit> {
|
||||
return PeriodicTask(
|
||||
delay = UPDATE_DELAY,
|
||||
task = {
|
||||
runSuspendCatching {
|
||||
val amountBlockState = (state.value as? OnrampV2MainComponentUM.Content)?.amountBlockState
|
||||
?: return@runSuspendCatching
|
||||
|
||||
val fiatAmount = amountBlockState.amountFieldModel.fiatAmount
|
||||
if (fiatAmount.value.isNullOrZero()) return@runSuspendCatching
|
||||
|
||||
fetchQuotesUseCase.invoke(
|
||||
userWallet = userWallet,
|
||||
amount = amountBlockState.amountFieldModel.fiatAmount,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
).onLeft(::handleOnrampError)
|
||||
}
|
||||
},
|
||||
onSuccess = {},
|
||||
onError = {},
|
||||
)
|
||||
}
|
||||
|
||||
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 subscribeOnOffers() = modelScope.launch {
|
||||
getOnrampOffersUseCase
|
||||
.invoke()
|
||||
.collectLatest { maybeOffers ->
|
||||
maybeOffers.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { offers ->
|
||||
val currentState = state.value
|
||||
if (currentState is OnrampV2MainComponentUM.Content) {
|
||||
if (currentState.amountBlockState.amountFieldModel.fiatValue.isEmpty()) {
|
||||
state.update {
|
||||
currentState.copy(offersBlockState = OnrampOffersBlockUM.Empty)
|
||||
}
|
||||
return@fold
|
||||
}
|
||||
state.update {
|
||||
onrampOffersStateFactory.getOffersState(offers)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToAmountChanges() = modelScope.launch {
|
||||
amountInputManager.query
|
||||
.filter(String::isNotEmpty)
|
||||
.collectLatest { _ ->
|
||||
startLoadingQuotes()
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToCountryAndCurrencyUpdates() {
|
||||
getOnrampCountryUseCase.invoke()
|
||||
.onEach { maybeCountry ->
|
||||
maybeCountry.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { country ->
|
||||
if (country == null) return@onEach
|
||||
state.update { prevState ->
|
||||
when (prevState) {
|
||||
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)
|
||||
when {
|
||||
quotes.isEmpty() -> {
|
||||
state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
}
|
||||
quotes.all { it is OnrampQuote.AmountError } -> {
|
||||
state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) }
|
||||
}
|
||||
quotes.none { it is OnrampQuote.Data } -> {
|
||||
state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
}
|
||||
else -> {
|
||||
state.update { prevState ->
|
||||
val resetState = amountStateFactory.getAmountSecondaryFieldResetState()
|
||||
if (prevState is OnrampV2MainComponentUM.Content &&
|
||||
resetState is OnrampV2MainComponentUM.Content &&
|
||||
prevState.offersBlockState is OnrampOffersBlockUM.Loading
|
||||
) {
|
||||
resetState.copy(offersBlockState = OnrampOffersBlockUM.Empty)
|
||||
} else {
|
||||
resetState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRetryQuotes() {
|
||||
state.update { prevState ->
|
||||
(prevState as? OnrampV2MainComponentUM.Content)?.copy(
|
||||
errorNotification = null,
|
||||
offersBlockState = OnrampOffersBlockUM.Loading,
|
||||
amountBlockState = prevState.amountBlockState.copy(
|
||||
secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
) ?: prevState
|
||||
}
|
||||
startLoadingQuotes()
|
||||
}
|
||||
|
||||
private suspend fun updatePairsAndQuotes() {
|
||||
fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = {
|
||||
state.update {
|
||||
amountStateFactory.getAmountSecondaryFieldResetState()
|
||||
}
|
||||
startLoadingQuotes()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleOnrampError(onrampError: OnrampError) {
|
||||
Timber.e(onrampError.toString())
|
||||
state.update { stateFactory.getOnrampErrorState(onrampError) }
|
||||
}
|
||||
|
||||
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 sendScreenOpenAnalytics() {
|
||||
analyticsEventHandler.send(
|
||||
OnrampAnalyticsEvent.ScreenOpened(
|
||||
source = params.source,
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendOfferClickEvent(
|
||||
quote: OnrampProviderWithQuote.Data,
|
||||
onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM,
|
||||
categoryUM: OnrampOfferCategoryUM,
|
||||
) {
|
||||
val event = when (categoryUM) {
|
||||
OnrampOfferCategoryUM.RecentlyUsed -> {
|
||||
OnrampAnalyticsEvent.RecentlyBuyClicked(
|
||||
tokenSymbol = params.cryptoCurrency.symbol,
|
||||
providerName = quote.provider.info.name,
|
||||
paymentMethod = quote.paymentMethod.name,
|
||||
)
|
||||
}
|
||||
OnrampOfferCategoryUM.Recommended -> {
|
||||
onrampOfferAdvantagesUM.toAnalyticsEvent(
|
||||
cryptoCurrencySymbol = params.cryptoCurrency.symbol,
|
||||
providerName = quote.provider.info.name,
|
||||
paymentMethodName = quote.paymentMethod.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (event != null) {
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val UPDATE_DELAY = 10_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InitialLoading(state: OnrampV2MainComponentUM.InitialLoading, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 16.dp),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
package com.tangem.features.onramp.mainv2.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
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.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.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.OnrampNewCurrencyUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampSecondaryFieldErrorUM
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM
|
||||
|
||||
@Composable
|
||||
internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = RoundedCornerShape(size = TangemTheme.dimens.radius16),
|
||||
)
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp)
|
||||
.animateContentSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
OnrampHeaderTitle()
|
||||
|
||||
OnrampAmountField(
|
||||
amountField = state.amountBlockState.amountFieldModel,
|
||||
currencyCode = state.amountBlockState.currencyUM.code,
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state.amountBlockState.secondaryFieldModel !is OnrampSecondaryFieldErrorUM.Empty,
|
||||
) {
|
||||
if (state.amountBlockState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Error) {
|
||||
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 = if (amountField.fiatValue.isBlank()) {
|
||||
TangemTheme.colors.text.disabled
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
),
|
||||
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.spacing8,
|
||||
bottom = TangemTheme.dimens.spacing4,
|
||||
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: OnrampSecondaryFieldErrorUM.Error) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = state.error.resolveReference(),
|
||||
color = TangemTheme.colors.text.warning,
|
||||
style = TangemTheme.typography.caption2,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,8 +17,6 @@ import com.tangem.core.decompose.context.childByContext
|
|||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.onramp.component.OnrampComponent
|
||||
import com.tangem.features.onramp.main.OnrampMainComponent
|
||||
import com.tangem.features.onramp.mainv2.OnrampV2MainComponent
|
||||
import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle
|
||||
import com.tangem.features.onramp.redirect.OnrampRedirectComponent
|
||||
import com.tangem.features.onramp.root.entity.OnrampChild
|
||||
import com.tangem.features.onramp.settings.OnrampSettingsComponent
|
||||
|
|
@ -32,9 +30,7 @@ internal class DefaultOnrampComponent @AssistedInject constructor(
|
|||
@Assisted private val params: OnrampComponent.Params,
|
||||
private val settingsComponentFactory: OnrampSettingsComponent.Factory,
|
||||
private val onrampMainComponentFactory: OnrampMainComponent.Factory,
|
||||
private val onrampMainV2ComponentFactory: OnrampV2MainComponent.Factory,
|
||||
private val onrampRedirectComponentFactory: OnrampRedirectComponent.Factory,
|
||||
private val onrampV2MainFeatureToggle: OnrampV2MainFeatureToggle,
|
||||
) : OnrampComponent, AppComponentContext by context {
|
||||
|
||||
private val navigation = StackNavigation<OnrampChild>()
|
||||
|
|
@ -71,44 +67,23 @@ internal class DefaultOnrampComponent @AssistedInject constructor(
|
|||
onBack = navigation::pop,
|
||||
),
|
||||
)
|
||||
OnrampChild.Main -> if (onrampV2MainFeatureToggle.isOnrampNewMainEnabled) {
|
||||
onrampMainV2ComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = OnrampV2MainComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
openSettings = { navigation.push(OnrampChild.Settings) },
|
||||
source = params.source,
|
||||
openRedirectPage = { quote ->
|
||||
navigation.push(
|
||||
OnrampChild.RedirectPage(
|
||||
quote = quote,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
onrampMainComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = OnrampMainComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
openSettings = { navigation.push(OnrampChild.Settings) },
|
||||
source = params.source,
|
||||
openRedirectPage = { onrampProviderWithQuoteData ->
|
||||
navigation.push(
|
||||
OnrampChild.RedirectPage(
|
||||
quote = onrampProviderWithQuoteData,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
),
|
||||
)
|
||||
},
|
||||
isLaunchSepa = params.shouldLaunchSepa,
|
||||
),
|
||||
)
|
||||
}
|
||||
OnrampChild.Main -> onrampMainComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = OnrampMainComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
openSettings = { navigation.push(OnrampChild.Settings) },
|
||||
source = params.source,
|
||||
openRedirectPage = { quote ->
|
||||
navigation.push(
|
||||
OnrampChild.RedirectPage(
|
||||
quote = quote,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = OnrampRedirectComponent.Params(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue