Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-05 14:58:00 +02:00
parent 94a4486443
commit 96ce2fc718
22 changed files with 784 additions and 57 deletions

View file

@ -1,11 +1,7 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.domain.onramp.repositories.LegacyTopUpRepository
import com.tangem.domain.onramp.* import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.HotCryptoRepository import com.tangem.domain.onramp.repositories.*
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -237,4 +233,32 @@ internal object OnrampDomainModule {
fun provideGetLegacyTopUpUrlUseCase(legacyTopUpRepository: LegacyTopUpRepository): GetLegacyTopUpUrlUseCase { fun provideGetLegacyTopUpUrlUseCase(legacyTopUpRepository: LegacyTopUpRepository): GetLegacyTopUpUrlUseCase {
return GetLegacyTopUpUrlUseCase(legacyTopUpRepository) return GetLegacyTopUpUrlUseCase(legacyTopUpRepository)
} }
@Provides
@Singleton
fun provideGetOnrampAllOffersUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
settingsRepository: SettingsRepository,
): GetOnrampAllOffersUseCase {
return GetOnrampAllOffersUseCase(
onrampRepository = onrampRepository,
errorResolver = onrampErrorResolver,
settingsRepository = settingsRepository,
)
}
@Provides
@Singleton
fun provideGetOnrampOffersUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
onrampTransactionRepository: OnrampTransactionRepository,
): GetOnrampOffersUseCase {
return GetOnrampOffersUseCase(
onrampRepository = onrampRepository,
errorResolver = onrampErrorResolver,
onrampTransactionRepository = onrampTransactionRepository,
)
}
} }

View file

@ -876,7 +876,10 @@
<string name="onramp_payment_method_subtitle">Payment method</string> <string name="onramp_payment_method_subtitle">Payment method</string>
<string name="onramp_provider_max_amount">Available up to %s</string> <string name="onramp_provider_max_amount">Available up to %s</string>
<string name="onramp_provider_min_amount">Available from %s</string> <string name="onramp_provider_min_amount">Available from %s</string>
<string name="onramp_providers_count">%d providers</string> <plurals name="onramp_providers_count">
<item quantity="one">%d provider</item>
<item quantity="other">%d providers</item>
</plurals>
<string name="onramp_providers_subtitle">Providers</string> <string name="onramp_providers_subtitle">Providers</string>
<string name="onramp_recently_used_title">Recently used</string> <string name="onramp_recently_used_title">Recently used</string>
<string name="onramp_recommended_title">Recommended</string> <string name="onramp_recommended_title">Recommended</string>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M8,11.848L11.332,13.901C11.857,14.225 12.506,13.744 12.366,13.134L11.482,9.264L14.426,6.66C14.89,6.25 14.642,5.472 14.031,5.419L10.155,5.084L8.639,1.432C8.4,0.856 7.6,0.856 7.361,1.432L5.845,5.084L1.969,5.419C1.358,5.472 1.11,6.25 1.574,6.66L4.518,9.264L3.634,13.134C3.494,13.744 4.142,14.225 4.668,13.901L8,11.848Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M10.495,1.6C10.535,1.462 10.357,1.368 10.265,1.478L3.851,9.114C3.778,9.2 3.84,9.333 3.953,9.333H6.953L5.505,14.398C5.466,14.537 5.643,14.631 5.735,14.521L12.149,6.885C12.222,6.798 12.161,6.666 12.047,6.666H9.048L10.495,1.6Z"
android:fillColor="#FFB71B"
android:fillType="evenOdd"/>
</vector>

View file

@ -1,13 +1,16 @@
package com.tangem.domain.onramp.model package com.tangem.domain.onramp.model
import java.math.BigDecimal
data class OnrampOffersBlock( data class OnrampOffersBlock(
val category: OnrampOfferCategory, val category: OnrampOfferCategory,
val offers: List<OnrampOffer>, val offers: List<OnrampOffer>,
val isVisible: Boolean = offers.isNotEmpty(), val hasMoreOffers: Boolean,
) )
data class OnrampOffer( data class OnrampOffer(
val quote: OnrampQuote, val quote: OnrampQuote,
val rateDif: BigDecimal?,
val advantages: OnrampOfferAdvantages = OnrampOfferAdvantages.Default, val advantages: OnrampOfferAdvantages = OnrampOfferAdvantages.Default,
) )

View file

@ -12,6 +12,7 @@ import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.utils.calculateRateDif
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -38,6 +39,7 @@ class GetOnrampAllOffersUseCase(
val isGooglePayAvailable = settingsRepository.isGooglePayAvailability() val isGooglePayAvailable = settingsRepository.isGooglePayAvailability()
val overallBestRateQuote = validQuotes.maxByOrNull { it.toAmount.value } val overallBestRateQuote = validQuotes.maxByOrNull { it.toAmount.value }
val bestRate = overallBestRateQuote?.toAmount?.value
val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod } val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod }
@ -48,7 +50,8 @@ class GetOnrampAllOffersUseCase(
} else { } else {
OnrampOfferAdvantages.Default OnrampOfferAdvantages.Default
} }
OnrampOffer(quote = quote, advantages = advantages) val rateDif = calculateRateDif(quote.toAmount.value, bestRate)
OnrampOffer(quote = quote, rateDif = rateDif, advantages = advantages)
} }
val groupBestRateOfferData = methodQuotes.maxByOrNull { it.toAmount.value } val groupBestRateOfferData = methodQuotes.maxByOrNull { it.toAmount.value }

View file

@ -11,6 +11,7 @@ import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.onramp.utils.calculateRateDif
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -43,8 +44,12 @@ class GetOnrampOffersUseCase(
val validQuotes = quotes.filterIsInstance<OnrampQuote.Data>() val validQuotes = quotes.filterIsInstance<OnrampQuote.Data>()
if (validQuotes.isEmpty()) return emptyList() if (validQuotes.isEmpty()) return emptyList()
val bestRateQuote = validQuotes.maxByOrNull { it.toAmount.value }
val bestRate = bestRateQuote?.toAmount?.value
val offers = validQuotes.map { quote -> val offers = validQuotes.map { quote ->
OnrampOffer(quote = quote) val rateDif = calculateRateDif(quote.toAmount.value, bestRate)
OnrampOffer(quote = quote, rateDif = rateDif)
} }
val recentOffer = findRecentOffer(offers, transactions) val recentOffer = findRecentOffer(offers, transactions)
@ -107,6 +112,15 @@ class GetOnrampOffersUseCase(
fastestOffer: OnrampOffer?, fastestOffer: OnrampOffer?,
allOffers: List<OnrampOffer>, allOffers: List<OnrampOffer>,
): List<OnrampOffersBlock> { ): List<OnrampOffersBlock> {
val recommendedOffers = buildRecommendedOffers(
recentOffer = recentOffer,
bestRateOffer = bestRateOffer,
fastestOffer = fastestOffer,
)
val shownOffersCount = (if (recentOffer != null) 1 else 0) + recommendedOffers.size
val hasMoreOffers = allOffers.size > shownOffersCount
return buildList { return buildList {
if (recentOffer != null) { if (recentOffer != null) {
add( add(
@ -119,23 +133,20 @@ class GetOnrampOffersUseCase(
bestRateOffer, bestRateOffer,
fastestOffer, fastestOffer,
), ),
rateDif = if (bestRateOffer != null) recentOffer.rateDif else null,
), ),
), ),
hasMoreOffers = false,
), ),
) )
} }
val recommendedOffers = buildRecommendedOffers(
recentOffer = recentOffer,
bestRateOffer = bestRateOffer,
fastestOffer = fastestOffer,
)
if (recommendedOffers.isNotEmpty() && hasOnlyOneMethodAndProvider(allOffers).not()) { if (recommendedOffers.isNotEmpty() && hasOnlyOneMethodAndProvider(allOffers).not()) {
add( add(
OnrampOffersBlock( OnrampOffersBlock(
category = OnrampOfferCategory.Recommended, category = OnrampOfferCategory.Recommended,
offers = recommendedOffers, offers = recommendedOffers,
hasMoreOffers = hasMoreOffers,
), ),
) )
} }
@ -167,17 +178,32 @@ class GetOnrampOffersUseCase(
return buildList { return buildList {
if (isSameOffer(bestRateOffer, fastestOffer)) { if (isSameOffer(bestRateOffer, fastestOffer)) {
bestRateOffer?.let { offer -> bestRateOffer?.let { offer ->
add(offer.copy(advantages = OnrampOfferAdvantages.BestRate)) add(
offer.copy(
advantages = OnrampOfferAdvantages.BestRate,
rateDif = null,
),
)
} }
} else { } else {
if (bestRateOffer != null && !isSameOffer(bestRateOffer, recentOffer)) { if (bestRateOffer != null && !isSameOffer(bestRateOffer, recentOffer)) {
add(bestRateOffer.copy(advantages = OnrampOfferAdvantages.BestRate)) add(
bestRateOffer.copy(
advantages = OnrampOfferAdvantages.BestRate,
rateDif = null,
),
)
} }
if (fastestOffer != null && !isSameOffer(fastestOffer, recentOffer) && if (fastestOffer != null && !isSameOffer(fastestOffer, recentOffer) &&
!isSameOffer(fastestOffer, bestRateOffer) !isSameOffer(fastestOffer, bestRateOffer)
) { ) {
add(fastestOffer.copy(advantages = OnrampOfferAdvantages.Fastest)) add(
fastestOffer.copy(
advantages = OnrampOfferAdvantages.Fastest,
rateDif = if (bestRateOffer != null) fastestOffer.rateDif else null,
),
)
} }
} }
} }

View file

@ -0,0 +1,8 @@
package com.tangem.domain.onramp.utils
import java.math.BigDecimal
internal fun calculateRateDif(currentTokenRate: BigDecimal, bestRate: BigDecimal?): BigDecimal? {
if (bestRate == null) return null
return BigDecimal.ONE - currentTokenRate / bestRate
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.onramp.alloffers
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
internal interface AllOffersComponent : ComposableBottomSheetComponent {
data class Params(
val userWallet: UserWallet,
val cryptoCurrency: CryptoCurrency,
val onDismiss: () -> Unit,
)
interface Factory : ComponentFactory<Params, AllOffersComponent>
}

View file

@ -0,0 +1,31 @@
package com.tangem.features.onramp.alloffers
import androidx.compose.runtime.Composable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.onramp.alloffers.model.AllOffersModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultAllOffersComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: AllOffersComponent.Params,
) : AllOffersComponent, AppComponentContext by context {
private val model: AllOffersModel = getOrCreateModel(params)
override fun dismiss() {
model.dismiss()
}
@Composable
override fun BottomSheet() {
// TODO to be continued in [REDACTED_TASK_KEY]
}
@AssistedFactory
interface Factory : AllOffersComponent.Factory {
override fun create(context: AppComponentContext, params: AllOffersComponent.Params): DefaultAllOffersComponent
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.onramp.alloffers.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onramp.alloffers.model.AllOffersModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface AllOffersComponentModelModule {
@Binds
@IntoMap
@ClassKey(AllOffersModel::class)
fun bindAllOffersModel(model: AllOffersModel): Model
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.onramp.alloffers.di
import com.tangem.features.onramp.alloffers.AllOffersComponent
import com.tangem.features.onramp.alloffers.DefaultAllOffersComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AllOffersComponentModule {
@Binds
@Singleton
fun bindAllOffersComponentFactory(factory: DefaultAllOffersComponent.Factory): AllOffersComponent.Factory
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.onramp.alloffers.model
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.onramp.alloffers.AllOffersComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import javax.inject.Inject
internal class AllOffersModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
) : Model() {
private val params: AllOffersComponent.Params = paramsContainer.require()
fun dismiss() {
params.onDismiss()
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent 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.confirmresidency.ConfirmResidencyComponent
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainBottomSheetConfig import com.tangem.features.onramp.mainv2.entity.OnrampV2MainBottomSheetConfig
import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel
@ -26,6 +27,7 @@ internal class DefaultOnrampV2MainComponent @AssistedInject constructor(
@Assisted private val params: OnrampV2MainComponent.Params, @Assisted private val params: OnrampV2MainComponent.Params,
private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory,
private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory, private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory,
private val allOffersComponentFactory: AllOffersComponent.Factory,
) : OnrampV2MainComponent, AppComponentContext by appComponentContext { ) : OnrampV2MainComponent, AppComponentContext by appComponentContext {
private val model: OnrampV2MainComponentModel = getOrCreateModel(params) private val model: OnrampV2MainComponentModel = getOrCreateModel(params)
@ -67,6 +69,14 @@ internal class DefaultOnrampV2MainComponent @AssistedInject constructor(
onDismiss = model.bottomSheetNavigation::dismiss, onDismiss = model.bottomSheetNavigation::dismiss,
), ),
) )
OnrampV2MainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create(
context = childByContext(componentContext),
params = AllOffersComponent.Params(
userWallet = model.userWallet,
cryptoCurrency = params.cryptoCurrency,
onDismiss = model.bottomSheetNavigation::dismiss,
),
)
} }
@AssistedFactory @AssistedFactory

View file

@ -6,5 +6,5 @@ class DefaultOnrampV2MainFeatureToggle(
private val featureTogglesManager: FeatureTogglesManager, private val featureTogglesManager: FeatureTogglesManager,
) : OnrampV2MainFeatureToggle { ) : OnrampV2MainFeatureToggle {
override val isOnrampNewMainEnabled: Boolean override val isOnrampNewMainEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_RECEIVE_ENABLED") get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_MAIN_ENABLED")
} }

View file

@ -1,6 +1,7 @@
package com.tangem.features.onramp.mainv2.entity package com.tangem.features.onramp.mainv2.entity
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.onramp.model.OnrampPaymentMethod import com.tangem.domain.onramp.model.OnrampPaymentMethod
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -20,22 +21,32 @@ internal sealed interface OnrampOffersBlockUM {
data class Content( data class Content(
override val isBlockVisible: Boolean, override val isBlockVisible: Boolean,
val offers: ImmutableList<OnrampOfferUM>, val recentOffer: OnrampOfferUM?,
val recommended: ImmutableList<OnrampOfferUM>,
val onrampAllOffersButtonConfig: OnrampAllOffersButtonConfig?,
) : OnrampOffersBlockUM ) : OnrampOffersBlockUM
} }
internal data class OnrampOfferUM( internal data class OnrampOfferUM(
val category: OnrampOfferCategory, val category: OnrampOfferCategoryUM,
val advantages: OnrampOfferAdvantages, val advantages: OnrampOfferAdvantagesUM,
val paymentMethod: OnrampPaymentMethod, val paymentMethod: OnrampPaymentMethod,
val providerId: String, val providerId: String,
val providerName: String, val providerName: String,
val rate: String,
val diff: TextReference?,
val onBuyClicked: () -> Unit,
) )
internal enum class OnrampOfferCategory { internal enum class OnrampOfferCategoryUM {
RecentlyUsed, Recommended RecentlyUsed, Recommended
} }
internal enum class OnrampOfferAdvantages { internal enum class OnrampOfferAdvantagesUM {
Default, BestRate, Fastest Default, BestRate, Fastest
} }
internal data class OnrampAllOffersButtonConfig(
val title: TextReference,
val onClick: () -> Unit,
)

View file

@ -10,4 +10,7 @@ sealed interface OnrampV2MainBottomSheetConfig {
@Serializable @Serializable
data object CurrenciesList : OnrampV2MainBottomSheetConfig data object CurrenciesList : OnrampV2MainBottomSheetConfig
@Serializable
data object AllOffers : OnrampV2MainBottomSheetConfig
} }

View file

@ -0,0 +1,106 @@
package com.tangem.features.onramp.mainv2.entity.factory
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.onramp.model.*
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.OnrampIntents
import com.tangem.features.onramp.mainv2.entity.*
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns.MINUS
import kotlinx.collections.immutable.toPersistentList
internal class OnrampOffersStateFactory(
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
private val onrampIntents: OnrampIntents,
) {
fun getOnShowOffersState(offers: List<OnrampOffersBlock>): OnrampV2MainComponentUM {
val currentState = currentStateProvider.invoke()
return when (currentState) {
is OnrampV2MainComponentUM.Content -> {
currentState.copy(
offersBlockState = mapOnrampOffersBlockToUM(offers),
)
}
is OnrampV2MainComponentUM.InitialLoading -> {
currentState
}
}
}
private fun mapOnrampOffersBlockToUM(offersBlocks: List<OnrampOffersBlock>): OnrampOffersBlockUM.Content {
val allOffersUM = mutableListOf<OnrampOfferUM>()
offersBlocks.map { block ->
block.offers.forEach { offer ->
when (val currentQuote = offer.quote) {
is OnrampQuote.AmountError,
is OnrampQuote.Error,
-> Unit
is OnrampQuote.Data -> {
allOffersUM.add(
OnrampOfferUM(
category = mapOfferCategoryDTOtoUM(block.category),
advantages = mapOfferAdvantagesDTOtoUM(offer.advantages),
paymentMethod = currentQuote.paymentMethod,
providerId = currentQuote.provider.id,
providerName = currentQuote.provider.info.name,
rate = currentQuote.toAmount.value.format {
crypto(
symbol = currentQuote.toAmount.symbol,
decimals = currentQuote.toAmount.decimals,
)
},
diff = offer.rateDif?.let { diff ->
stringReference("$MINUS${diff.format { percent() }}")
},
onBuyClicked = {
onrampIntents.onBuyClick(
quote = OnrampProviderWithQuote.Data(
provider = offer.quote.provider,
paymentMethod = currentQuote.paymentMethod,
toAmount = currentQuote.toAmount,
fromAmount = currentQuote.fromAmount,
),
)
},
),
)
}
}
}
}
return OnrampOffersBlockUM.Content(
isBlockVisible = false,
recentOffer = allOffersUM.firstOrNull { it.category == OnrampOfferCategoryUM.RecentlyUsed },
recommended = allOffersUM.filter { it.category == OnrampOfferCategoryUM.Recommended }.toPersistentList(),
onrampAllOffersButtonConfig = if (offersBlocks.any { it.hasMoreOffers }) {
OnrampAllOffersButtonConfig(
title = TextReference.Res(R.string.onramp_all_offers_button_title),
onClick = { onrampIntents.openProviders() },
)
} else {
null
},
)
}
private fun mapOfferCategoryDTOtoUM(category: OnrampOfferCategory): OnrampOfferCategoryUM {
return when (category) {
OnrampOfferCategory.Recent -> OnrampOfferCategoryUM.RecentlyUsed
OnrampOfferCategory.Recommended -> OnrampOfferCategoryUM.Recommended
}
}
private fun mapOfferAdvantagesDTOtoUM(advantages: OnrampOfferAdvantages): OnrampOfferAdvantagesUM {
return when (advantages) {
OnrampOfferAdvantages.Default -> OnrampOfferAdvantagesUM.Default
OnrampOfferAdvantages.BestRate -> OnrampOfferAdvantagesUM.BestRate
OnrampOfferAdvantages.Fastest -> OnrampOfferAdvantagesUM.Fastest
}
}
}

View file

@ -20,6 +20,7 @@ import com.tangem.features.onramp.main.entity.OnrampLastUpdate
import com.tangem.features.onramp.mainv2.OnrampV2MainComponent import com.tangem.features.onramp.mainv2.OnrampV2MainComponent
import com.tangem.features.onramp.mainv2.entity.* import com.tangem.features.onramp.mainv2.entity.*
import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory 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.OnrampV2AmountStateFactory
import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2StateFactory import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2StateFactory
import com.tangem.features.onramp.utils.sendOnrampErrorEvent import com.tangem.features.onramp.utils.sendOnrampErrorEvent
@ -32,7 +33,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@Suppress("LongParameterList") @Suppress("LongParameterList", "LargeClass")
internal class OnrampV2MainComponentModel @Inject constructor( internal class OnrampV2MainComponentModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
@ -44,6 +45,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase, private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase,
private val fetchPairsUseCase: OnrampFetchPairsUseCase, private val fetchPairsUseCase: OnrampFetchPairsUseCase,
private val amountInputManager: InputManager, private val amountInputManager: InputManager,
private val getOnrampOffersUseCase: GetOnrampOffersUseCase,
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
getWalletsUseCase: GetWalletsUseCase, getWalletsUseCase: GetWalletsUseCase,
) : Model(), OnrampIntents { ) : Model(), OnrampIntents {
@ -58,6 +60,13 @@ internal class OnrampV2MainComponentModel @Inject constructor(
OnrampAmountButtonUMStateFactory() OnrampAmountButtonUMStateFactory()
} }
private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) {
OnrampOffersStateFactory(
currentStateProvider = Provider { _state.value },
onrampIntents = this,
)
}
private val stateFactory = OnrampV2StateFactory( private val stateFactory = OnrampV2StateFactory(
currentStateProvider = Provider { _state.value }, currentStateProvider = Provider { _state.value },
cryptoCurrency = params.cryptoCurrency, cryptoCurrency = params.cryptoCurrency,
@ -94,6 +103,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
subscribeToAmountChanges() subscribeToAmountChanges()
subscribeToCountryAndCurrencyUpdates() subscribeToCountryAndCurrencyUpdates()
subscribeToQuotesUpdate() subscribeToQuotesUpdate()
subscribeOnOffers()
} }
override fun onDestroy() { override fun onDestroy() {
@ -117,11 +127,19 @@ internal class OnrampV2MainComponentModel @Inject constructor(
} }
override fun onBuyClick(quote: OnrampProviderWithQuote.Data) { override fun onBuyClick(quote: OnrampProviderWithQuote.Data) {
// TODO in [REDACTED_TASK_KEY] to be continued 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,
),
)
params.openRedirectPage(quote)
} }
override fun openProviders() { override fun openProviders() {
// TODO in [REDACTED_TASK_KEY] to be continued bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.AllOffers)
} }
override fun onRefresh() { override fun onRefresh() {
@ -172,6 +190,20 @@ internal class OnrampV2MainComponentModel @Inject constructor(
router.pop() router.pop()
} }
private fun subscribeOnOffers() = modelScope.launch {
getOnrampOffersUseCase.invoke(
userWalletId = userWallet.walletId,
cryptoCurrencyId = params.cryptoCurrency.id,
).collectLatest { maybeOffers ->
maybeOffers.fold(
ifLeft = ::handleOnrampError,
ifRight = { offers ->
_state.update { onrampOffersStateFactory.getOnShowOffersState(offers) }
},
)
}
}
private fun subscribeToAmountChanges() = modelScope.launch { private fun subscribeToAmountChanges() = modelScope.launch {
amountInputManager.query amountInputManager.query
.filter(String::isNotEmpty) .filter(String::isNotEmpty)

View file

@ -1,13 +1,42 @@
package com.tangem.features.onramp.mainv2.ui package com.tangem.features.onramp.mainv2.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
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.OnrampOfferAdvantagesUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM
import kotlinx.collections.immutable.persistentListOf
@Composable @Composable
internal fun OnrampOffersContent(state: OnrampOffersBlockUM) { internal fun OnrampOffersContent(state: OnrampOffersBlockUM) {
@ -23,10 +52,330 @@ internal fun OnrampOffersContent(state: OnrampOffersBlockUM) {
), ),
label = "Offers block animation", label = "Offers block animation",
) { ) {
Text( if (state is OnrampOffersBlockUM.Content) {
text = "Some offers", Column(modifier = Modifier.fillMaxWidth()) {
style = TangemTheme.typography.head, state.recentOffer?.let { recentOffer ->
Column {
Text(
modifier = Modifier.padding(start = 12.dp),
text = stringResourceSafe(R.string.onramp_recently_used_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
SpacerH(8.dp)
Offer(recentOffer)
SpacerH(16.dp)
}
}
Text(
modifier = Modifier.padding(start = 12.dp),
text = stringResourceSafe(R.string.onramp_recommended_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
SpacerH(8.dp)
state.recommended.fastForEach {
Offer(it)
SpacerH(8.dp)
}
state.onrampAllOffersButtonConfig?.let {
SpacerH(12.dp)
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = it.title.resolveReference(),
onClick = it.onClick,
)
}
}
}
}
}
@Composable
private fun Offer(onrampOfferUM: OnrampOfferUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors.background.action,
shape = RoundedCornerShape(14.dp),
)
.padding(12.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
OfferHeader(advantage = onrampOfferUM.advantages)
RateBlock(rate = onrampOfferUM.rate, diff = onrampOfferUM.diff)
}
SpacerWMax()
SecondaryButton(
size = TangemButtonSize.RoundedAction,
text = stringResourceSafe(R.string.common_buy),
onClick = onrampOfferUM.onBuyClicked,
)
}
SpacerH(10.dp)
HorizontalDivider(color = TangemTheme.colors.stroke.primary)
SpacerH(12.dp)
PaymentBlockInOffer(
paymentMethod = onrampOfferUM.paymentMethod,
providerName = onrampOfferUM.providerName,
) )
// TODO in [REDACTED_TASK_KEY] to be continued }
}
@Composable
private fun OfferHeader(advantage: OnrampOfferAdvantagesUM) {
when (advantage) {
OnrampOfferAdvantagesUM.Default -> {
Text(
text = stringResourceSafe(R.string.onramp_title_you_get),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
OnrampOfferAdvantagesUM.BestRate -> {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_best_rate_16),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
Text(
text = stringResourceSafe(R.string.express_provider_best_rate),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.icon.accent,
)
}
}
OnrampOfferAdvantagesUM.Fastest -> {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_fastest_16),
tint = TangemTheme.colors.icon.attention,
contentDescription = null,
)
Text(
text = stringResourceSafe(R.string.onramp_offer_type_fastet),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.icon.attention,
)
}
}
}
}
@Composable
private fun RateBlock(rate: String, diff: TextReference?) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = rate,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
diff?.let {
Text(
modifier = Modifier
.background(
color = TangemTheme.colors.text.warning.copy(alpha = 0.1f),
shape = RoundedCornerShape(4.dp),
)
.padding(horizontal = 4.dp),
text = it.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.warning,
)
}
}
}
@Composable
private fun PaymentBlockInOffer(paymentMethod: OnrampPaymentMethod, providerName: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_clock_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerW(2.dp)
TimingBlock(speed = paymentMethod.type.getProcessingSpeed())
SpacerW(6.dp)
DrawDot(color = TangemTheme.colors.text.tertiary)
SpacerW(6.dp)
Text(
text = providerName,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
SpacerWMax()
Text(
text = stringResourceSafe(R.string.onramp_pay_with),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
SpacerW(4.dp)
SubcomposeAsyncImage(
modifier = Modifier.sizeIn(maxWidth = 38.dp, maxHeight = 16.dp),
model = ImageRequest.Builder(context = LocalContext.current)
.data(paymentMethod.imageUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
loading = {
TextShimmer(
style = TangemTheme.typography.body1,
modifier = Modifier.width(40.dp),
)
},
contentDescription = null,
)
}
}
@Composable
private fun TimingBlock(speed: PaymentMethodType.PaymentSpeed) {
val timingText = when (speed) {
PaymentMethodType.PaymentSpeed.Instant -> {
stringResourceSafe(id = R.string.onramp_instant_status)
}
PaymentMethodType.PaymentSpeed.FewMin -> {
stringResourceSafe(
id = R.string.onramp_timing_minutes,
FEW_MINS_VALUE,
)
}
PaymentMethodType.PaymentSpeed.FewDays -> {
pluralStringResourceSafe(
id = R.plurals.onramp_timing_days,
count = FEW_DAYS_VALUE,
FEW_DAYS_VALUE,
)
}
PaymentMethodType.PaymentSpeed.PlentyDays -> {
pluralStringResourceSafe(
id = R.plurals.onramp_timing_days,
count = PLENTY_DAYS_VALUE,
PLENTY_DAYS_VALUE,
)
}
PaymentMethodType.PaymentSpeed.Unknown -> ""
}
Text(
text = timingText,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
@Composable
fun DrawDot(color: Color) {
Spacer(
modifier = Modifier
.size(4.dp)
.drawWithCache {
val radius = size.minDimension / 2f
onDrawBehind {
drawCircle(
color = color,
radius = radius,
)
}
},
)
}
// will be hardcoded till server will be ready to provide this values
private const val FEW_MINS_VALUE = "3-5"
private const val FEW_DAYS_VALUE = 3
private const val PLENTY_DAYS_VALUE = 5
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun OnrampOffersContentPreview() {
val state = OnrampOffersBlockUM.Content(
isBlockVisible = true,
recentOffer = OnrampOfferUM(
category = OnrampOfferCategoryUM.RecentlyUsed,
advantages = OnrampOfferAdvantagesUM.Default,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId3",
providerName = "Simplex",
rate = "0,00045334 BTC",
diff = stringReference("27%"),
onBuyClicked = {},
),
recommended = persistentListOf(
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.BestRate,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId1",
providerName = "Simplex",
rate = "0,0245334 BTC",
diff = null,
onBuyClicked = {},
),
OnrampOfferUM(
category = OnrampOfferCategoryUM.Recommended,
advantages = OnrampOfferAdvantagesUM.Fastest,
paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
providerId = "providerId2",
providerName = "Simplex",
rate = "0,00145334 BTC",
diff = stringReference("0.07%"),
onBuyClicked = {},
),
),
onrampAllOffersButtonConfig = null,
)
TangemThemePreview {
OnrampOffersContent(state)
} }
} }

View file

@ -1,5 +1,6 @@
package com.tangem.features.onramp.mainv2.ui package com.tangem.features.onramp.mainv2.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -51,6 +52,7 @@ internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modif
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth()
.background( .background(
color = TangemTheme.colors.background.action, color = TangemTheme.colors.background.action,
shape = RoundedCornerShape(size = TangemTheme.dimens.radius16), shape = RoundedCornerShape(size = TangemTheme.dimens.radius16),
@ -61,16 +63,14 @@ internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modif
) { ) {
OnrampHeaderTitle() OnrampHeaderTitle()
SpacerH(12.dp)
OnrampAmountField( OnrampAmountField(
amountField = state.amountBlockState.amountFieldModel, amountField = state.amountBlockState.amountFieldModel,
currencyCode = state.amountBlockState.currencyUM.code, currencyCode = state.amountBlockState.currencyUM.code,
) )
SpacerH(8.dp) AnimatedVisibility(!state.offersBlockState.isBlockVisible) {
OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel)
OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel) }
SpacerH(20.dp) SpacerH(20.dp)
@ -115,7 +115,8 @@ private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: Strin
modifier = Modifier modifier = Modifier
.focusRequester(requester) .focusRequester(requester)
.padding( .padding(
top = TangemTheme.dimens.spacing24, top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing4,
start = TangemTheme.dimens.spacing12, start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12,
) )

View file

@ -17,6 +17,8 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.onramp.component.OnrampComponent import com.tangem.features.onramp.component.OnrampComponent
import com.tangem.features.onramp.main.OnrampMainComponent 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.redirect.OnrampRedirectComponent
import com.tangem.features.onramp.root.entity.OnrampChild import com.tangem.features.onramp.root.entity.OnrampChild
import com.tangem.features.onramp.settings.OnrampSettingsComponent import com.tangem.features.onramp.settings.OnrampSettingsComponent
@ -24,13 +26,15 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember") @Suppress("LongParameterList")
internal class DefaultOnrampComponent @AssistedInject constructor( internal class DefaultOnrampComponent @AssistedInject constructor(
@Assisted context: AppComponentContext, @Assisted context: AppComponentContext,
@Assisted private val params: OnrampComponent.Params, @Assisted private val params: OnrampComponent.Params,
private val settingsComponentFactory: OnrampSettingsComponent.Factory, private val settingsComponentFactory: OnrampSettingsComponent.Factory,
private val onrampMainComponentFactory: OnrampMainComponent.Factory, private val onrampMainComponentFactory: OnrampMainComponent.Factory,
private val onrampMainV2ComponentFactory: OnrampV2MainComponent.Factory,
private val onrampRedirectComponentFactory: OnrampRedirectComponent.Factory, private val onrampRedirectComponentFactory: OnrampRedirectComponent.Factory,
private val onrampV2MainFeatureToggle: OnrampV2MainFeatureToggle,
) : OnrampComponent, AppComponentContext by context { ) : OnrampComponent, AppComponentContext by context {
private val navigation = StackNavigation<OnrampChild>() private val navigation = StackNavigation<OnrampChild>()
@ -66,24 +70,44 @@ internal class DefaultOnrampComponent @AssistedInject constructor(
onBack = navigation::pop, onBack = navigation::pop,
), ),
) )
OnrampChild.Main -> onrampMainComponentFactory.create( OnrampChild.Main -> if (onrampV2MainFeatureToggle.isOnrampNewMainEnabled) {
context = childByContext(componentContext), onrampMainV2ComponentFactory.create(
params = OnrampMainComponent.Params( context = childByContext(componentContext),
userWalletId = params.userWalletId, params = OnrampV2MainComponent.Params(
cryptoCurrency = params.cryptoCurrency, userWalletId = params.userWalletId,
openSettings = { navigation.push(OnrampChild.Settings) }, cryptoCurrency = params.cryptoCurrency,
source = params.source, openSettings = { navigation.push(OnrampChild.Settings) },
openRedirectPage = { source = params.source,
navigation.push( openRedirectPage = {
OnrampChild.RedirectPage( navigation.push(
quote = it, OnrampChild.RedirectPage(
cryptoCurrency = params.cryptoCurrency, quote = it,
), cryptoCurrency = params.cryptoCurrency,
) ),
}, )
launchSepa = params.launchSepa, },
), ),
) )
} else {
onrampMainComponentFactory.create(
context = childByContext(componentContext),
params = OnrampMainComponent.Params(
userWalletId = params.userWalletId,
cryptoCurrency = params.cryptoCurrency,
openSettings = { navigation.push(OnrampChild.Settings) },
source = params.source,
openRedirectPage = {
navigation.push(
OnrampChild.RedirectPage(
quote = it,
cryptoCurrency = params.cryptoCurrency,
),
)
},
launchSepa = params.launchSepa,
),
)
}
is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create( is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create(
context = childByContext(componentContext), context = childByContext(componentContext),
params = OnrampRedirectComponent.Params( params = OnrampRedirectComponent.Params(