Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-22 19:35:21 +02:00
parent ddfe8e2789
commit 3773f5f8c9
9 changed files with 150 additions and 37 deletions

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.features.marketing.api.MarketingBannerComponent
internal interface AllOffersComponent : ComposableBottomSheetComponent {
@ -14,6 +15,12 @@ internal interface AllOffersComponent : ComposableBottomSheetComponent {
val onDismiss: () -> Unit,
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
val amountCurrencyCode: String,
// Marketing banner components are created and owned by the parent onramp-main component and passed
// down so this sheet reuses their models (and their amount-gated request flows) instead of building
// its own: [marketingBannerComponent] renders the standalone banner, [linkedMarketingBannerComponent]
// renders the per-provider LINKED_TO_PROVIDER banner next to each offer.
val marketingBannerComponent: MarketingBannerComponent,
val linkedMarketingBannerComponent: MarketingBannerComponent,
)
interface Factory : ComponentFactory<Params, AllOffersComponent>

View file

@ -13,7 +13,7 @@ import dagger.assisted.AssistedInject
internal class DefaultAllOffersComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: AllOffersComponent.Params,
@Assisted private val params: AllOffersComponent.Params,
) : AllOffersComponent, AppComponentContext by context {
private val model: AllOffersModel = getOrCreateModel(params)
@ -27,6 +27,8 @@ internal class DefaultAllOffersComponent @AssistedInject constructor(
val state by model.state.collectAsState()
AllOffersContentSheet(
state = state,
marketingBannerComponent = params.marketingBannerComponent,
linkedMarketingBannerComponent = params.linkedMarketingBannerComponent,
onCloseClick = { dismiss() },
)
}

View file

@ -25,9 +25,11 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemeRedesign
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.PaymentMethodStatus
import com.tangem.domain.onramp.model.PaymentMethodType
import com.tangem.features.marketing.api.MarketingBannerComponent
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
@ -35,13 +37,18 @@ import com.tangem.features.onramp.impl.R
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 com.tangem.features.onramp.main.ui.OfferWithLinkedBanner
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@Composable
internal fun AllOffersContentSheet(state: AllOffersStateUM, onCloseClick: () -> Unit) {
internal fun AllOffersContentSheet(
state: AllOffersStateUM,
marketingBannerComponent: MarketingBannerComponent,
linkedMarketingBannerComponent: MarketingBannerComponent,
onCloseClick: () -> Unit,
) {
val onBack = remember(state) {
{
if (state is AllOffersStateUM.Content && state.currentMethod != null) {
@ -71,37 +78,64 @@ internal fun AllOffersContentSheet(state: AllOffersStateUM, onCloseClick: () ->
}
},
content = {
Box(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 8.dp)
.animateContentSize(),
) {
AnimatedContent(
targetState = state is AllOffersStateUM.Content && state.currentMethod != null,
transitionSpec = {
fadeIn(tween(durationMillis = 220)) togetherWith
fadeOut(tween(durationMillis = 220))
},
label = "Change offers and payment method state",
) { shouldShowOffersScreen ->
when (state) {
AllOffersStateUM.Loading -> AllOffersContentLoading()
is AllOffersStateUM.Error -> AllOffersError(state.errorNotification)
is AllOffersStateUM.Content -> {
if (shouldShowOffersScreen) {
state.currentMethod?.let {
OffersBasedOnPaymentMethodContent(offers = it.offers)
}
} else {
PaymentMethodsContent(methods = state.methods)
AllOffersSheetContent(
state = state,
marketingBannerComponent = marketingBannerComponent,
linkedMarketingBannerComponent = linkedMarketingBannerComponent,
)
},
)
}
@Composable
private fun AllOffersSheetContent(
state: AllOffersStateUM,
marketingBannerComponent: MarketingBannerComponent,
linkedMarketingBannerComponent: MarketingBannerComponent,
) {
Column(modifier = Modifier.fillMaxSize()) {
// Standalone marketing banner at the top of the sheet (DS3 -> wrap in the redesign theme).
// Renders nothing when no matching campaign, so it adds no space in the common case.
TangemThemeRedesign {
marketingBannerComponent.Content(
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 8.dp)
.animateContentSize(),
) {
AnimatedContent(
targetState = state is AllOffersStateUM.Content && state.currentMethod != null,
transitionSpec = {
fadeIn(tween(durationMillis = 220)) togetherWith
fadeOut(tween(durationMillis = 220))
},
label = "Change offers and payment method state",
) { shouldShowOffersScreen ->
when (state) {
AllOffersStateUM.Loading -> AllOffersContentLoading()
is AllOffersStateUM.Error -> AllOffersError(state.errorNotification)
is AllOffersStateUM.Content -> {
if (shouldShowOffersScreen) {
state.currentMethod?.let { method ->
OffersBasedOnPaymentMethodContent(
offers = method.offers,
linkedMarketingBannerComponent = linkedMarketingBannerComponent,
)
}
} else {
PaymentMethodsContent(methods = state.methods)
}
}
}
}
},
)
}
}
}
@Composable
@ -127,7 +161,10 @@ private fun PaymentMethodTitle(onCloseClick: () -> Unit) {
}
@Composable
private fun OffersBasedOnPaymentMethodContent(offers: ImmutableList<OnrampOfferUM>) {
private fun OffersBasedOnPaymentMethodContent(
offers: ImmutableList<OnrampOfferUM>,
linkedMarketingBannerComponent: MarketingBannerComponent,
) {
Column(
modifier = Modifier
.fillMaxWidth()
@ -136,7 +173,7 @@ private fun OffersBasedOnPaymentMethodContent(offers: ImmutableList<OnrampOfferU
) {
offers.fastForEach { offer ->
key("${offer.paymentMethod.id} ${offer.providerName} ${offer.rate}") {
Offer(offer)
OfferWithLinkedBanner(offer, linkedMarketingBannerComponent)
SpacerH(8.dp)
}
}
@ -250,11 +287,18 @@ private fun AllOffersContentSheetPaymentPreview() {
currentMethod = method,
onBackClicked = {},
),
marketingBannerComponent = PreviewMarketingBannerComponent,
linkedMarketingBannerComponent = PreviewMarketingBannerComponent,
onCloseClick = {},
)
}
}
private val PreviewMarketingBannerComponent = object : MarketingBannerComponent {
@Composable
override fun Content(modifier: Modifier) = Unit
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -319,6 +363,8 @@ private fun AllOffersContentSheetOffersPreview() {
currentMethod = null,
onBackClicked = {},
),
marketingBannerComponent = PreviewMarketingBannerComponent,
linkedMarketingBannerComponent = PreviewMarketingBannerComponent,
onCloseClick = {},
)
}

View file

@ -106,6 +106,8 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
onDismiss = model.bottomSheetNavigation::dismiss,
openRedirectPage = params.openRedirectPage,
amountCurrencyCode = config.amountCurrencyCode,
marketingBannerComponent = marketingBannerComponent,
linkedMarketingBannerComponent = linkedMarketingBannerComponent,
),
)
}

View file

@ -104,7 +104,7 @@ internal fun OnrampOffersContent(state: OnrampOffersBlockUM, linkedMarketingBann
}
@Composable
private fun OfferWithLinkedBanner(offer: OnrampOfferUM, linkedMarketingBannerComponent: MarketingBannerComponent) {
internal fun OfferWithLinkedBanner(offer: OnrampOfferUM, linkedMarketingBannerComponent: MarketingBannerComponent) {
val hasBanner = linkedMarketingBannerComponent.hasLinkedBanner(offer.providerId)
// Square the offer's bottom corners so the bottom-rounded banner glues to it as one card.
Offer(offer, roundBottom = !hasBanner)