Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-10 09:44:21 +02:00
parent 3e250db77c
commit f899b889e6
26 changed files with 383 additions and 371 deletions

View file

@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.* import com.tangem.core.ui.components.*
@ -38,6 +39,9 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.core.ui.utils.toPx import com.tangem.core.ui.utils.toPx
/** Default reserved height for the [TangemModalBottomSheetWithFooter] footer slot. */
val DEFAULT_FOOTER_HEIGHT: Dp = 80.dp
/** /**
* Modal bottom sheet with [content], [footer] and optional [title]. * Modal bottom sheet with [content], [footer] and optional [title].
* *
@ -50,6 +54,12 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheetWi
config: TangemBottomSheetConfig, config: TangemBottomSheetConfig,
containerColor: Color = TangemTheme.colors.background.primary, containerColor: Color = TangemTheme.colors.background.primary,
skipPartiallyExpanded: Boolean = true, skipPartiallyExpanded: Boolean = true,
// FIXME([REDACTED_TASK_KEY]): temp workaround
// The footer slot reserves a fixed [DEFAULT_FOOTER_HEIGHT];
// callers whose footer differs must pass the real height explicitly. Exposed as an opt-in
// parameter so existing usages keep the previous behavior and nothing else is affected.
// Rework so the sheet measures the actual footer height internally and drops this parameter.
footerHeight: Dp = DEFAULT_FOOTER_HEIGHT,
noinline onBack: (() -> Unit)? = null, noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit = {}, crossinline title: @Composable BoxScope.(T) -> Unit = {},
crossinline content: @Composable (T) -> Unit, crossinline content: @Composable (T) -> Unit,
@ -65,6 +75,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheetWi
content = content, content = content,
footer = footer, footer = footer,
skipPartiallyExpanded = skipPartiallyExpanded, skipPartiallyExpanded = skipPartiallyExpanded,
footerHeight = footerHeight,
) )
} else { } else {
DefaultModalBottomSheetWithFooter<T>( DefaultModalBottomSheetWithFooter<T>(
@ -75,6 +86,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheetWi
footer = footer, footer = footer,
onBack = onBack, onBack = onBack,
skipPartiallyExpanded = skipPartiallyExpanded, skipPartiallyExpanded = skipPartiallyExpanded,
footerHeight = footerHeight,
) )
} }
} }
@ -85,6 +97,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheetW
config: TangemBottomSheetConfig, config: TangemBottomSheetConfig,
containerColor: Color, containerColor: Color,
skipPartiallyExpanded: Boolean = true, skipPartiallyExpanded: Boolean = true,
footerHeight: Dp = DEFAULT_FOOTER_HEIGHT,
noinline onBack: (() -> Unit)? = null, noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit, crossinline title: @Composable BoxScope.(T) -> Unit,
crossinline content: @Composable (T) -> Unit, crossinline content: @Composable (T) -> Unit,
@ -117,6 +130,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheetW
onBack = onBack, onBack = onBack,
content = content, content = content,
footer = footer, footer = footer,
footerHeight = footerHeight,
) )
} }
@ -135,6 +149,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetW
config: TangemBottomSheetConfig, config: TangemBottomSheetConfig,
containerColor: Color, containerColor: Color,
skipPartiallyExpanded: Boolean = true, skipPartiallyExpanded: Boolean = true,
footerHeight: Dp = DEFAULT_FOOTER_HEIGHT,
crossinline title: @Composable BoxScope.(T) -> Unit, crossinline title: @Composable BoxScope.(T) -> Unit,
crossinline content: @Composable (T) -> Unit, crossinline content: @Composable (T) -> Unit,
noinline footer: @Composable (BoxScope.(T) -> Unit)?, noinline footer: @Composable (BoxScope.(T) -> Unit)?,
@ -150,6 +165,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetW
title = title, title = title,
content = content, content = content,
footer = footer, footer = footer,
footerHeight = footerHeight,
) )
} }
@ -161,6 +177,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
sheetState: TangemSheetState, sheetState: TangemSheetState,
containerColor: Color, containerColor: Color,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
footerHeight: Dp = DEFAULT_FOOTER_HEIGHT,
noinline onBack: (() -> Unit)? = null, noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit, crossinline title: @Composable BoxScope.(T) -> Unit,
crossinline content: @Composable (T) -> Unit, crossinline content: @Composable (T) -> Unit,
@ -178,11 +195,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
val isKeyboardOpen by rememberIsKeyboardVisible() val isKeyboardOpen by rememberIsKeyboardVisible()
val buttonHeight by animateDpAsState( val buttonHeight by animateDpAsState(
if (footer != null) { targetValue = if (footer != null) footerHeight else 0.dp,
80.dp label = "FooterHeight",
} else {
0.dp
},
) )
// Offset calculation for keyboard scroll adjustment: // Offset calculation for keyboard scroll adjustment:
// 1) Button height (footer) // 1) Button height (footer)

View file

@ -23,6 +23,7 @@ dependencies {
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency)
implementation(projects.domain.account.status) implementation(projects.domain.account.status)
implementation(projects.domain.promo)
/** Core */ /** Core */
api(projects.core.configToggles) api(projects.core.configToggles)

View file

@ -1,12 +1,11 @@
package com.tangem.features.promobanners.impl.campaigns.component package com.tangem.features.promobanners.impl.campaigns.component
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.child
@ -14,13 +13,10 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.core.ui.ds2.button.Close import com.tangem.core.ui.ds2.button.Close
import com.tangem.core.ui.ds2.button.TangemButton import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
import com.tangem.features.promobanners.impl.campaigns.model.ActivateCampaignsModel import com.tangem.features.promobanners.impl.campaigns.model.ActivateCampaignsModel
@ -32,7 +28,8 @@ internal class ActivateCampaignBottomSheetComponent(
chooseTokenComponentFactory: ChooseTokenComponent.Factory, chooseTokenComponentFactory: ChooseTokenComponent.Factory,
private val params: Params, private val params: Params,
val onDismiss: () -> Unit, val onDismiss: () -> Unit,
) : CampaignsModularComponent, AppComponentContext by appComponentContext { val onFooterExtraHeightReady: (Dp) -> Unit,
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
private val model: ActivateCampaignsModel = getOrCreateModel(params) private val model: ActivateCampaignsModel = getOrCreateModel(params)
@ -42,7 +39,7 @@ internal class ActivateCampaignBottomSheetComponent(
) )
@Composable @Composable
override fun Title(bottomSheetState: State<BottomSheetState>) { override fun Title() {
TangemTopNavigation( TangemTopNavigation(
windowInsets = WindowInsets(0), windowInsets = WindowInsets(0),
blurBackground = false, blurBackground = false,
@ -51,10 +48,10 @@ internal class ActivateCampaignBottomSheetComponent(
} }
@Composable @Composable
override fun Content(bottomSheetState: State<BottomSheetState>, contentPadding: PaddingValues, modifier: Modifier) { override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle() val state by model.uiState.collectAsStateWithLifecycle()
ActivateCampaignContent(um = state) ActivateCampaignContent(um = state, modifier = modifier)
if (state.isChoosingToken) { if (state.isChoosingToken) {
ChooseTokenBottomSheet(state.onChooseTokenDismiss) ChooseTokenBottomSheet(state.onChooseTokenDismiss)
@ -67,6 +64,7 @@ internal class ActivateCampaignBottomSheetComponent(
ActivateCampaignFooter( ActivateCampaignFooter(
footerUM = state.footerUM, footerUM = state.footerUM,
onFooterTextHeightReady = onFooterExtraHeightReady,
) )
} }
@ -90,6 +88,6 @@ internal class ActivateCampaignBottomSheetComponent(
interface ActivateCampaignModelCallbacks { interface ActivateCampaignModelCallbacks {
val onActivated: (CampaignType) -> Unit val onActivated: (CampaignType) -> Unit
val onAlreadyActivated: (CampaignType, AppCurrency, Account?, CryptoCurrencyStatus) -> Unit val onAlreadyActivated: (CampaignType) -> Unit
} }
} }

View file

@ -1,39 +1,33 @@
package com.tangem.features.promobanners.impl.campaigns.component package com.tangem.features.promobanners.impl.campaigns.component
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.core.ui.ds2.button.Close import com.tangem.core.ui.ds2.button.Close
import com.tangem.core.ui.ds2.button.TangemButton import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.promobanners.impl.R import com.tangem.features.promobanners.impl.R
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
import com.tangem.features.promobanners.impl.campaigns.model.CampaignAlreadyActivatedModel import com.tangem.features.promobanners.impl.campaigns.entity.CampaignTypeToContentConverter
import com.tangem.features.promobanners.impl.campaigns.ui.ActivateCampaignContent import com.tangem.features.promobanners.impl.campaigns.ui.AlreadyActivatedCampaignContent
internal class CampaignAlreadyActivatedBottomSheetComponent( internal class CampaignAlreadyActivatedBottomSheetComponent(
appComponentContext: AppComponentContext, appComponentContext: AppComponentContext,
params: Params, params: Params,
val onDismiss: () -> Unit, val onDismiss: () -> Unit,
) : CampaignsModularComponent, AppComponentContext by appComponentContext { ) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
private val model: CampaignAlreadyActivatedModel = getOrCreateModel(params) private val campaignName = CampaignTypeToContentConverter().convert(params.campaignType).name
@Composable @Composable
override fun Title(bottomSheetState: State<BottomSheetState>) { override fun Title() {
TangemTopNavigation( TangemTopNavigation(
windowInsets = WindowInsets(0), windowInsets = WindowInsets(0),
blurBackground = false, blurBackground = false,
@ -42,10 +36,14 @@ internal class CampaignAlreadyActivatedBottomSheetComponent(
} }
@Composable @Composable
override fun Content(bottomSheetState: State<BottomSheetState>, contentPadding: PaddingValues, modifier: Modifier) { override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle() AlreadyActivatedCampaignContent(
message = resourceReference(
ActivateCampaignContent(um = state) R.string.promo_campaign_already_activated_title,
wrappedList(campaignName),
),
modifier = modifier,
)
} }
@Composable @Composable
@ -59,8 +57,5 @@ internal class CampaignAlreadyActivatedBottomSheetComponent(
data class Params( data class Params(
val campaignType: CampaignType, val campaignType: CampaignType,
val account: Account?,
val appCurrency: AppCurrency,
val currency: CryptoCurrencyStatus,
) )
} }

View file

@ -1,30 +1,31 @@
package com.tangem.features.promobanners.impl.campaigns.component package com.tangem.features.promobanners.impl.campaigns.component
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.core.ui.ds2.button.Close import com.tangem.core.ui.ds2.button.Close
import com.tangem.core.ui.ds2.button.TangemButton import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.promobanners.impl.R import com.tangem.features.promobanners.impl.R
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
import com.tangem.features.promobanners.impl.campaigns.entity.campaignName import com.tangem.features.promobanners.impl.campaigns.entity.CampaignTypeToContentConverter
import com.tangem.features.promobanners.impl.campaigns.ui.CampaignEnrolledMessageContent import com.tangem.features.promobanners.impl.campaigns.ui.CampaignEnrolledMessageContent
internal class CampaignEnrolledBottomSheetComponent( internal class CampaignEnrolledBottomSheetComponent(
private val campaignType: CampaignType, params: Params,
private val onDismissRequest: () -> Unit, private val onDismissRequest: () -> Unit,
) : CampaignsModularComponent { ) : ComposableModularContentComponent {
private val campaignName = CampaignTypeToContentConverter().convert(params.campaignType).name
@Composable @Composable
override fun Title(bottomSheetState: State<BottomSheetState>) { override fun Title() {
TangemTopNavigation( TangemTopNavigation(
windowInsets = WindowInsets(0), windowInsets = WindowInsets(0),
blurBackground = false, blurBackground = false,
@ -33,9 +34,13 @@ internal class CampaignEnrolledBottomSheetComponent(
} }
@Composable @Composable
override fun Content(bottomSheetState: State<BottomSheetState>, contentPadding: PaddingValues, modifier: Modifier) { override fun Content(modifier: Modifier) {
CampaignEnrolledMessageContent( CampaignEnrolledMessageContent(
message = stringReference("You're successfully enrolled in ${campaignType.campaignName()}"), message = resourceReference(
R.string.promo_campaign_enroll_success_title,
wrappedList(campaignName),
),
modifier = modifier,
) )
} }
@ -47,4 +52,8 @@ internal class CampaignEnrolledBottomSheetComponent(
onClick = { onDismissRequest() }, onClick = { onDismissRequest() },
) )
} }
data class Params(
val campaignType: CampaignType,
)
} }

View file

@ -1,10 +0,0 @@
package com.tangem.features.promobanners.impl.campaigns.component
import androidx.compose.runtime.Composable
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
interface CampaignsModularComponent : ComposableModularBottomSheetContentComponent {
@Composable
fun Footer()
}

View file

@ -2,33 +2,26 @@ package com.tangem.features.promobanners.impl.campaigns.component
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.childSlot
import com.tangem.core.decompose.context.AppComponentContext 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.components.bottomsheets.LocalBottomSheetContentScrollable
import com.tangem.core.ui.components.bottomsheets.LocalTangemBottomSheetContentBottomInset
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.DEFAULT_FOOTER_HEIGHT
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.core.ui.extensions.rememberLastNonNull import com.tangem.core.ui.extensions.rememberLastNonNull
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
import com.tangem.features.promobanners.api.swapcashback.CampaignsComponent import com.tangem.features.promobanners.api.swapcashback.CampaignsComponent
import com.tangem.features.promobanners.impl.campaigns.component.ActivateCampaignBottomSheetComponent.ActivateCampaignModelCallbacks import com.tangem.features.promobanners.impl.campaigns.component.ActivateCampaignBottomSheetComponent.ActivateCampaignModelCallbacks
@ -59,41 +52,23 @@ internal class DefaultCampaignsComponent @AssistedInject constructor(
val bottomSheet by bottomSheetSlot.subscribeAsState() val bottomSheet by bottomSheetSlot.subscribeAsState()
val activeChild = bottomSheet.child?.instance val activeChild = bottomSheet.child?.instance
val displayedChild = rememberLastNonNull(activeChild) val displayedChild = rememberLastNonNull(activeChild)
val bottomSheetState = remember { mutableStateOf(BottomSheetState.EXPANDED) } val footerExtraHeight by model.footerExtraHeightState.collectAsStateWithLifecycle()
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>( TangemModalBottomSheetWithFooter<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig( config = TangemBottomSheetConfig(
isShown = activeChild != null, isShown = activeChild != null,
onDismissRequest = model::onDismiss, onDismissRequest = model::onDismiss,
content = TangemBottomSheetConfigContent.Empty, content = TangemBottomSheetConfigContent.Empty,
), ),
containerColor = TangemTheme.colors3.bg.primary,
footerHeight = DEFAULT_FOOTER_HEIGHT + footerExtraHeight,
onBack = model::onDismiss, onBack = model::onDismiss,
title = { title = {
displayedChild?.Title(bottomSheetState) displayedChild?.Title()
}, },
content = { content = {
val bottomInset = LocalTangemBottomSheetContentBottomInset.current Box(modifier = Modifier.animateContentSize()) {
val scrollableSignal = LocalBottomSheetContentScrollable.current displayedChild?.Content(modifier = Modifier)
if (scrollableSignal != null) {
LaunchedEffect(Unit) {
scrollableSignal.value = false
}
DisposableEffect(scrollableSignal) {
onDispose { scrollableSignal.value = true }
}
}
Box(
modifier = Modifier
.padding(bottom = bottomInset)
.animateContentSize(),
) {
displayedChild?.Content(
bottomSheetState = bottomSheetState,
contentPadding = PaddingValues(),
modifier = Modifier,
)
} }
}, },
footer = { footer = {
@ -111,42 +86,37 @@ internal class DefaultCampaignsComponent @AssistedInject constructor(
private fun bottomSheetChild( private fun bottomSheetChild(
config: CampaignsBottomSheetConfig, config: CampaignsBottomSheetConfig,
componentContext: ComponentContext, componentContext: ComponentContext,
): CampaignsModularComponent { ): ComposableModularContentComponent {
val context = childByContext(componentContext) val context = childByContext(componentContext)
return when (config) { return when (config) {
CampaignsBottomSheetConfig.NotActive -> NotActiveCampaignBottomSheetComponent( CampaignsBottomSheetConfig.NotActive -> NotActiveCampaignBottomSheetComponent(
onDismissRequest = model::onDismiss, onDismissRequest = model::onDismiss,
) )
is CampaignsBottomSheetConfig.Enrolled -> CampaignEnrolledBottomSheetComponent( is CampaignsBottomSheetConfig.Enrolled -> CampaignEnrolledBottomSheetComponent(
campaignType = config.campaignType, params = CampaignEnrolledBottomSheetComponent.Params(
campaignType = config.campaignType,
),
onDismissRequest = model::onDismiss, onDismissRequest = model::onDismiss,
) )
is CampaignsBottomSheetConfig.Activate -> ActivateCampaignBottomSheetComponent( is CampaignsBottomSheetConfig.Activate -> ActivateCampaignBottomSheetComponent(
appComponentContext = context, appComponentContext = context,
chooseTokenComponentFactory = chooseTokenComponentFactory, chooseTokenComponentFactory = chooseTokenComponentFactory,
onDismiss = model::onDismiss, onDismiss = model::onDismiss,
onFooterExtraHeightReady = model::onFooterExtraHeightReady,
params = ActivateCampaignBottomSheetComponent.Params( params = ActivateCampaignBottomSheetComponent.Params(
campaignType = config.campaignType, campaignType = config.campaignType,
modelCallbacks = object : ActivateCampaignModelCallbacks { modelCallbacks = object : ActivateCampaignModelCallbacks {
override val onActivated: (CampaignType) -> Unit = model::onActivated override val onActivated: (CampaignType) -> Unit = model::onActivated
override val onAlreadyActivated: ( override val onAlreadyActivated: (CampaignType) -> Unit = model::onAlreadyActivated
CampaignType,
AppCurrency,
Account?,
CryptoCurrencyStatus,
) -> Unit = model::onAlreadyActivated
}, },
), ),
) )
is CampaignsBottomSheetConfig.AlreadyActivated -> CampaignAlreadyActivatedBottomSheetComponent( is CampaignsBottomSheetConfig.AlreadyActivated -> CampaignAlreadyActivatedBottomSheetComponent(
appComponentContext = context, appComponentContext = context,
onDismiss = model::onDismiss,
params = CampaignAlreadyActivatedBottomSheetComponent.Params( params = CampaignAlreadyActivatedBottomSheetComponent.Params(
campaignType = config.campaignType, campaignType = config.campaignType,
appCurrency = config.appCurrency,
account = config.account,
currency = config.currency,
), ),
onDismiss = model::onDismiss,
) )
} }
} }

View file

@ -1,13 +1,11 @@
package com.tangem.features.promobanners.impl.campaigns.component package com.tangem.features.promobanners.impl.campaigns.component
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.core.ui.ds2.button.Close import com.tangem.core.ui.ds2.button.Close
import com.tangem.core.ui.ds2.button.TangemButton import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
@ -17,10 +15,10 @@ import com.tangem.features.promobanners.impl.campaigns.ui.NotActiveCampaignMessa
internal class NotActiveCampaignBottomSheetComponent( internal class NotActiveCampaignBottomSheetComponent(
private val onDismissRequest: () -> Unit, private val onDismissRequest: () -> Unit,
) : CampaignsModularComponent { ) : ComposableModularContentComponent {
@Composable @Composable
override fun Title(bottomSheetState: State<BottomSheetState>) { override fun Title() {
TangemTopNavigation( TangemTopNavigation(
windowInsets = WindowInsets(0), windowInsets = WindowInsets(0),
blurBackground = false, blurBackground = false,
@ -29,8 +27,8 @@ internal class NotActiveCampaignBottomSheetComponent(
} }
@Composable @Composable
override fun Content(bottomSheetState: State<BottomSheetState>, contentPadding: PaddingValues, modifier: Modifier) { override fun Content(modifier: Modifier) {
NotActiveCampaignMessageContent() NotActiveCampaignMessageContent(modifier = modifier)
} }
@Composable @Composable

View file

@ -1,6 +1,7 @@
package com.tangem.features.promobanners.impl.campaigns.deeplink package com.tangem.features.promobanners.impl.campaigns.deeplink
import com.tangem.common.routing.deeplink.DeeplinkConst import com.tangem.common.routing.deeplink.DeeplinkConst
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.promobanners.api.deeplink.CampaignsDeepLinkHandler import com.tangem.features.promobanners.api.deeplink.CampaignsDeepLinkHandler
import com.tangem.features.promobanners.api.toggles.PromoBannersFeatureToggles import com.tangem.features.promobanners.api.toggles.PromoBannersFeatureToggles
import com.tangem.features.promobanners.impl.campaigns.service.CampaignsService import com.tangem.features.promobanners.impl.campaigns.service.CampaignsService
@ -18,13 +19,24 @@ import dagger.assisted.AssistedInject
internal class DefaultCampaignsDeepLinkHandler @AssistedInject constructor( internal class DefaultCampaignsDeepLinkHandler @AssistedInject constructor(
@Assisted private val queryParams: Map<String, String>, @Assisted private val queryParams: Map<String, String>,
campaignsService: CampaignsService, campaignsService: CampaignsService,
private val promoBannersFeatureToggles: PromoBannersFeatureToggles, getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
promoBannersFeatureToggles: PromoBannersFeatureToggles,
) : CampaignsDeepLinkHandler { ) : CampaignsDeepLinkHandler {
init { init {
if (promoBannersFeatureToggles.isCampaignsToggleEnabled) { if (promoBannersFeatureToggles.isCampaignsToggleEnabled) {
val campaignId = queryParams[DeeplinkConst.CAMPAIGN_ID_KEY].orEmpty() // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
campaignsService.show(campaignId) getSelectedWalletSyncUseCase().fold(
ifLeft = {
TangemLogger.e("Error on getting user wallet")
},
ifRight = { userWallet ->
val campaignId = queryParams[DeeplinkConst.CAMPAIGN_ID_KEY].orEmpty()
val userWalletId = userWallet.walletId
campaignsService.show(campaignId = campaignId, userWalletId = userWalletId)
},
)
} else { } else {
TangemLogger.i("Campaigns feature is disabled") TangemLogger.i("Campaigns feature is disabled")
} }

View file

@ -7,7 +7,6 @@ import com.tangem.features.promobanners.api.swapcashback.CampaignsComponent
import com.tangem.features.promobanners.impl.campaigns.component.DefaultCampaignsComponent import com.tangem.features.promobanners.impl.campaigns.component.DefaultCampaignsComponent
import com.tangem.features.promobanners.impl.campaigns.deeplink.DefaultCampaignsDeepLinkHandler import com.tangem.features.promobanners.impl.campaigns.deeplink.DefaultCampaignsDeepLinkHandler
import com.tangem.features.promobanners.impl.campaigns.model.ActivateCampaignsModel import com.tangem.features.promobanners.impl.campaigns.model.ActivateCampaignsModel
import com.tangem.features.promobanners.impl.campaigns.model.CampaignAlreadyActivatedModel
import com.tangem.features.promobanners.impl.campaigns.model.CampaignsModel import com.tangem.features.promobanners.impl.campaigns.model.CampaignsModel
import com.tangem.features.promobanners.impl.campaigns.service.CampaignsService import com.tangem.features.promobanners.impl.campaigns.service.CampaignsService
import com.tangem.features.promobanners.impl.campaigns.service.DefaultCampaignsService import com.tangem.features.promobanners.impl.campaigns.service.DefaultCampaignsService
@ -51,9 +50,4 @@ internal interface CampaignsModelModule {
@IntoMap @IntoMap
@ClassKey(ActivateCampaignsModel::class) @ClassKey(ActivateCampaignsModel::class)
fun bindCampaignActivateModel(model: ActivateCampaignsModel): Model fun bindCampaignActivateModel(model: ActivateCampaignsModel): Model
@Binds
@IntoMap
@ClassKey(CampaignAlreadyActivatedModel::class)
fun bindCampaignAlreadyActivatedModel(model: CampaignAlreadyActivatedModel): Model
} }

View file

@ -3,6 +3,7 @@ package com.tangem.features.promobanners.impl.campaigns.entity
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
/** /**
@ -19,6 +20,7 @@ import com.tangem.core.ui.extensions.TextReference
@Immutable @Immutable
internal data class ActivateCampaignUM( internal data class ActivateCampaignUM(
val logo: TangemIconUM,
val title: TextReference, val title: TextReference,
val description: TextReference, val description: TextReference,
val selectedToken: TokenItemState?, val selectedToken: TokenItemState?,
@ -27,6 +29,7 @@ internal data class ActivateCampaignUM(
val footerUM: FooterUM, val footerUM: FooterUM,
val onChooseTokenDismiss: () -> Unit, val onChooseTokenDismiss: () -> Unit,
val onLearnMoreClick: () -> Unit, val onLearnMoreClick: () -> Unit,
val onChooseTokenClick: () -> Unit,
) )
@Immutable @Immutable

View file

@ -1,10 +0,0 @@
package com.tangem.features.promobanners.impl.campaigns.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.token.state.TokenItemState
@Immutable
internal data class CampaignAlreadyActivatedUM(
val selectedToken: TokenItemState,
val selectedAccount: SelectedAccountUM?,
)

View file

@ -1,7 +1,39 @@
package com.tangem.features.promobanners.impl.campaigns.entity package com.tangem.features.promobanners.impl.campaigns.entity
internal fun CampaignType.campaignName(): String = when (this) { import com.tangem.core.ui.ds.image.TangemIconUM
// TODO([REDACTED_TASK_KEY]): source real campaign display names. import com.tangem.core.ui.extensions.resourceReference
is CampaignType.ReactivationCashback -> "Reactivation Cashback" import com.tangem.domain.promo.models.PromoCampaignId
is CampaignType.WhaleSwapCashback -> "Whale Swap Cashback" import com.tangem.features.promobanners.impl.R
import com.tangem.features.promobanners.impl.campaigns.model.CampaignContent
import com.tangem.utils.converter.Converter
internal class CampaignTypeToContentConverter : Converter<CampaignType, CampaignContent> {
override fun convert(value: CampaignType): CampaignContent = when (value) {
is CampaignType.ReactivationCashback -> CampaignContent(
name = "Summer Swap Cashback",
logo = TangemIconUM.Url(
url = "https://s3.dualstack.eu-central-1.amazonaws.com/tangem.api/stories/Reactivation_Cashback.webp",
fallbackRes = R.drawable.ic_alert_24,
),
description = resourceReference(R.string.promo_campaign_reactivation_summary_description),
termsUrl = "https://tangem.com/docs/en/summer-swap-cashback-terms.pdf",
learnMoreUrl = "https://tangem.com/en/blog/post/summer-swap",
)
is CampaignType.WhaleSwapCashback -> CampaignContent(
name = "Whale Swap Cashback",
logo = TangemIconUM.Url(
url = "https://s3.dualstack.eu-central-1.amazonaws.com/tangem.api/stories/Whale_Swap_Cashback.webp",
fallbackRes = R.drawable.ic_alert_24,
),
description = resourceReference(R.string.promo_campaign_whale_swap_summary_description),
termsUrl = "https://tangem.com/docs/en/whale-swap-cashback-terms.pdf",
learnMoreUrl = "https://tangem.com/en/blog/post/whale-swap",
)
}
}
internal fun CampaignType.toPromoCampaignId(): PromoCampaignId = when (this) {
is CampaignType.ReactivationCashback -> PromoCampaignId.ReactivationCashback
is CampaignType.WhaleSwapCashback -> PromoCampaignId.WhaleSwapCashback
} }

View file

@ -1,8 +1,5 @@
package com.tangem.features.promobanners.impl.campaigns.entity package com.tangem.features.promobanners.impl.campaigns.entity
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -24,8 +21,5 @@ internal sealed class CampaignsBottomSheetConfig {
@Serializable @Serializable
data class AlreadyActivated( data class AlreadyActivated(
val campaignType: CampaignType, val campaignType: CampaignType,
val appCurrency: AppCurrency,
val account: Account?,
val currency: CryptoCurrencyStatus,
) : CampaignsBottomSheetConfig() ) : CampaignsBottomSheetConfig()
} }

View file

@ -3,29 +3,40 @@ package com.tangem.features.promobanners.impl.campaigns.model
import com.tangem.common.ui.account.AccountIconItemStateConverter import com.tangem.common.ui.account.AccountIconItemStateConverter
import com.tangem.common.ui.account.toUM import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.resourceReference 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.message.SnackbarMessage
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.promo.models.EnrollResult
import com.tangem.domain.promo.models.PromoCampaignId
import com.tangem.domain.promo.models.TokenReward
import com.tangem.domain.promo.usecase.EnrollPromoCampaignUseCase
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
import com.tangem.features.promobanners.impl.R import com.tangem.features.promobanners.impl.R
import com.tangem.features.promobanners.impl.campaigns.component.ActivateCampaignBottomSheetComponent import com.tangem.features.promobanners.impl.campaigns.component.ActivateCampaignBottomSheetComponent
import com.tangem.features.promobanners.impl.campaigns.entity.ActivateCampaignUM import com.tangem.features.promobanners.impl.campaigns.entity.ActivateCampaignUM
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignTypeToContentConverter
import com.tangem.features.promobanners.impl.campaigns.entity.FooterUM import com.tangem.features.promobanners.impl.campaigns.entity.FooterUM
import com.tangem.features.promobanners.impl.campaigns.entity.SelectedAccountUM import com.tangem.features.promobanners.impl.campaigns.entity.SelectedAccountUM
import com.tangem.features.promobanners.impl.campaigns.entity.TermsUM import com.tangem.features.promobanners.impl.campaigns.entity.TermsUM
import com.tangem.features.promobanners.impl.campaigns.entity.campaignName import com.tangem.features.promobanners.impl.campaigns.entity.toPromoCampaignId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -35,6 +46,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped @ModelScoped
internal class ActivateCampaignsModel @Inject constructor( internal class ActivateCampaignsModel @Inject constructor(
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
@ -42,17 +54,20 @@ internal class ActivateCampaignsModel @Inject constructor(
chooseTokenBridgeFactory: ChooseTokenBridge.Factory, chooseTokenBridgeFactory: ChooseTokenBridge.Factory,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val enrollPromoCampaignUseCase: EnrollPromoCampaignUseCase,
private val urlOpener: UrlOpener, private val urlOpener: UrlOpener,
@GlobalUiMessageSender private val messageSender: UiMessageSender,
) : Model() { ) : Model() {
private val params = paramsContainer.require<ActivateCampaignBottomSheetComponent.Params>() private val params = paramsContainer.require<ActivateCampaignBottomSheetComponent.Params>()
private val campaignType = params.campaignType
private val accountIconConverter = AccountIconItemStateConverter(size = AccountIconSize.ExtraSmall) private val accountIconConverter = AccountIconItemStateConverter(size = AccountIconSize.ExtraSmall)
private var appCurrency: AppCurrency = AppCurrency.Default private var appCurrency: AppCurrency = AppCurrency.Default
private var selectedAccount: Account? = null private val campaignId: PromoCampaignId = params.campaignType.toPromoCampaignId()
private var selectedCurrency: CryptoCurrencyStatus? = null private val campaignContent = CampaignTypeToContentConverter().convert(campaignType)
val uiState: StateFlow<ActivateCampaignUM> val uiState: StateFlow<ActivateCampaignUM>
field = MutableStateFlow(getInitialState()) field = MutableStateFlow(buildInitialModel())
val bridge: ChooseTokenBridge = chooseTokenBridgeFactory.create( val bridge: ChooseTokenBridge = chooseTokenBridgeFactory.create(
modelScope = modelScope, modelScope = modelScope,
@ -78,7 +93,26 @@ internal class ActivateCampaignsModel @Inject constructor(
.launchIn(modelScope) .launchIn(modelScope)
} }
private fun onSelectTokenClick() { private fun buildInitialModel(): ActivateCampaignUM = ActivateCampaignUM(
logo = campaignContent.logo,
title = resourceReference(
R.string.promo_campaign_summary_title,
wrappedList(campaignContent.name),
),
description = campaignContent.description,
selectedToken = null,
selectedAccount = null,
isChoosingToken = false,
footerUM = FooterUM(
label = resourceReference(R.string.promo_campaign_select_token),
onPrimaryButtonClick = ::onChooseTokenClick,
),
onChooseTokenDismiss = ::onChooseTokenDismiss,
onLearnMoreClick = ::onLearnMoreClick,
onChooseTokenClick = ::onChooseTokenClick,
)
private fun onChooseTokenClick() {
uiState.update { it.copy(isChoosingToken = true) } uiState.update { it.copy(isChoosingToken = true) }
} }
@ -86,42 +120,45 @@ internal class ActivateCampaignsModel @Inject constructor(
uiState.update { it.copy(isChoosingToken = false) } uiState.update { it.copy(isChoosingToken = false) }
} }
private fun onEnrollClick() { private fun onEnrollClick(selectedWalletId: UserWalletId, selectedCurrencyStatus: CryptoCurrencyStatus) {
val token = selectedCurrencyStatus.currency as? CryptoCurrency.Token ?: return
modelScope.launch { modelScope.launch {
// TODO([REDACTED_TASK_KEY]): call the real campaign enrollment use case; its result decides the next sheet. enrollPromoCampaignUseCase.invoke(
// Success -> the "enrolled" sheet; "already activated" error -> hand the chosen token/account campaign = campaignId,
// over to the "already activated" sheet. tokenReward = TokenReward(
if (enrollInCampaign()) { tokenAddress = token.contractAddress,
params.modelCallbacks.onActivated(params.campaignType) networkId = token.network.rawId,
} else { ),
selectedCurrency?.let { walletIds = listOf(selectedWalletId),
params.modelCallbacks.onAlreadyActivated(params.campaignType, appCurrency, selectedAccount, it) ).onLeft { error ->
} TangemLogger.e("Error enrolling campaign ${campaignType.campaignId}", error)
messageSender.send(SnackbarMessage(message = resourceReference(R.string.common_unknown_error)))
}.onRight {
handleEnrollResponse(it)
} }
} }
} }
@Suppress("FunctionOnlyReturningConstant") // TODO([REDACTED_TASK_KEY]): stub until the real enrollment use case exists. private fun handleEnrollResponse(enrollResult: EnrollResult) {
private fun enrollInCampaign(): Boolean { when (enrollResult) {
// TODO([REDACTED_TASK_KEY]): replace with the real enrollment use case call; `true` = enrolled, `false` = already active. is EnrollResult.AlreadyEnrolled -> params.modelCallbacks.onAlreadyActivated(campaignType)
return true is EnrollResult.Success -> params.modelCallbacks.onActivated(campaignType)
}
} }
private fun onTermsClick() { private fun onTermsClick() {
urlOpener.openUrl(CAMPAIGN_TERMS_URL) urlOpener.openUrl(campaignContent.termsUrl)
} }
private fun onLearnMoreClick() { private fun onLearnMoreClick() {
urlOpener.openUrl(CAMPAIGN_TERMS_URL) urlOpener.openUrl(campaignContent.learnMoreUrl)
} }
private fun onTokenChosen(result: ChooseTokenResult) { private fun onTokenChosen(result: ChooseTokenResult) {
modelScope.launch { modelScope.launch {
val selectedAccountUM = if (isAccountsModeEnabledUseCase.invokeSync()) { val selectedAccountUM = if (isAccountsModeEnabledUseCase.invokeSync()) {
val account = result.account.account when (val account = result.account.account) {
selectedAccount = account
when (account) {
is Account.CryptoPortfolio -> SelectedAccountUM( is Account.CryptoPortfolio -> SelectedAccountUM(
iconState = accountIconConverter.convert(account), iconState = accountIconConverter.convert(account),
name = account.accountName.toUM().value, name = account.accountName.toUM().value,
@ -136,7 +173,6 @@ internal class ActivateCampaignsModel @Inject constructor(
null null
} }
selectedCurrency = result.currency
val tokenItem = TokenItemStateConverter(appCurrency = appCurrency).convert(result.currency) val tokenItem = TokenItemStateConverter(appCurrency = appCurrency).convert(result.currency)
uiState.update { state -> uiState.update { state ->
@ -145,12 +181,19 @@ internal class ActivateCampaignsModel @Inject constructor(
selectedToken = tokenItem, selectedToken = tokenItem,
selectedAccount = selectedAccountUM, selectedAccount = selectedAccountUM,
footerUM = FooterUM( footerUM = FooterUM(
label = stringReference("Enroll"), label = resourceReference(R.string.promo_campaign_enroll),
onPrimaryButtonClick = ::onEnrollClick, onPrimaryButtonClick = {
onEnrollClick(
selectedWalletId = result.walletId,
selectedCurrencyStatus = result.currency,
)
},
terms = TermsUM( terms = TermsUM(
// TODO([REDACTED_TASK_KEY]): localize text = resourceReference(R.string.promo_campaign_terms_agreement_android),
text = stringReference("I agree with"), linkText = resourceReference(
linkText = stringReference("${params.campaignType.campaignName()} Terms"), R.string.promo_campaign_terms_link,
wrappedList(campaignContent.name),
),
onTermsClick = ::onTermsClick, onTermsClick = ::onTermsClick,
), ),
), ),
@ -158,29 +201,4 @@ internal class ActivateCampaignsModel @Inject constructor(
} }
} }
} }
private fun getInitialState(): ActivateCampaignUM {
return ActivateCampaignUM(
// TODO([REDACTED_TASK_KEY]): source real campaign copy.
title = stringReference("Enroll in ${params.campaignType.campaignName()}"),
description = stringReference(
"Earn cashback on every swap from \$10K until the end of July.\n\n" +
"Rates step up with size: 0.10% from \$10K, 0.20% from \$20K, 0.50% from \$100K.",
),
selectedToken = null,
selectedAccount = null,
isChoosingToken = false,
footerUM = FooterUM(
label = stringReference("Select token"),
onPrimaryButtonClick = ::onSelectTokenClick,
),
onChooseTokenDismiss = ::onChooseTokenDismiss,
onLearnMoreClick = ::onLearnMoreClick,
)
}
private companion object {
// TODO([REDACTED_TASK_KEY]): replace with the real campaign terms URL.
const val CAMPAIGN_TERMS_URL = "https://tangem.com/en/"
}
} }

View file

@ -1,63 +0,0 @@
package com.tangem.features.promobanners.impl.campaigns.model
import com.tangem.common.ui.account.AccountIconItemStateConverter
import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.domain.models.account.Account
import com.tangem.features.promobanners.impl.campaigns.component.CampaignAlreadyActivatedBottomSheetComponent
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignAlreadyActivatedUM
import com.tangem.features.promobanners.impl.campaigns.entity.SelectedAccountUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
/**
* Thin model for the "campaign already activated" bottom sheet. The screen is read-only: it just renders
* the token/account the cashback is paid out to.
*
* The state is the selection the user made in the activate flow, handed over via [Params] when enrollment
* reports the campaign is already active. That selection is in-memory only (it carries non-serializable UI
* state), so after process death it is `null` and the model assembles a placeholder state instead
* (see [REDACTED_TASK_KEY]).
*/
@ModelScoped
internal class CampaignAlreadyActivatedModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<CampaignAlreadyActivatedBottomSheetComponent.Params>()
private val accountIconConverter = AccountIconItemStateConverter(size = AccountIconSize.ExtraSmall)
val uiState: StateFlow<CampaignAlreadyActivatedUM>
field = MutableStateFlow(buildState())
private fun buildState(): CampaignAlreadyActivatedUM {
val selectedAccountUM = params.account?.let { account ->
when (account) {
is Account.CryptoPortfolio -> SelectedAccountUM(
iconState = accountIconConverter.convert(account),
name = account.accountName.toUM().value,
)
is Account.Payment -> SelectedAccountUM(
iconState = CurrencyIconState.PaymentAccount(size = AccountIconSize.ExtraSmall),
name = account.accountName.toUM().value,
)
is Account.Virtual -> null
}
}
val tokenItem = TokenItemStateConverter(appCurrency = params.appCurrency).convert(params.currency)
return CampaignAlreadyActivatedUM(
selectedToken = tokenItem,
selectedAccount = selectedAccountUM,
)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.promobanners.impl.campaigns.model
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.TextReference
internal data class CampaignContent(
val name: String,
val logo: TangemIconUM,
val description: TextReference,
val termsUrl: String,
val learnMoreUrl: String,
)

View file

@ -1,20 +1,32 @@
package com.tangem.features.promobanners.impl.campaigns.model package com.tangem.features.promobanners.impl.campaigns.model
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.domain.models.account.Account import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.promo.models.PromoCampaignState
import com.tangem.domain.promo.usecase.GetPromoCampaignStateUseCase
import com.tangem.features.promobanners.impl.R
import com.tangem.features.promobanners.impl.campaigns.converters.CampaignIdConverter import com.tangem.features.promobanners.impl.campaigns.converters.CampaignIdConverter
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignsBottomSheetConfig import com.tangem.features.promobanners.impl.campaigns.entity.CampaignsBottomSheetConfig
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
import com.tangem.features.promobanners.impl.campaigns.entity.toPromoCampaignId
import com.tangem.features.promobanners.impl.campaigns.service.CampaignsService import com.tangem.features.promobanners.impl.campaigns.service.CampaignsService
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ModelScoped @ModelScoped
@ -22,35 +34,62 @@ internal class CampaignsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val campaignIdConverter: CampaignIdConverter, private val campaignIdConverter: CampaignIdConverter,
campaignsService: CampaignsService, campaignsService: CampaignsService,
private val getPromoCampaignStateUseCase: GetPromoCampaignStateUseCase,
@GlobalUiMessageSender private val messageSender: UiMessageSender,
) : Model() { ) : Model() {
val bottomSheetNavigation: SlotNavigation<CampaignsBottomSheetConfig> = SlotNavigation() val bottomSheetNavigation: SlotNavigation<CampaignsBottomSheetConfig> = SlotNavigation()
val footerExtraHeightState: StateFlow<Dp>
field = MutableStateFlow(0.dp)
init { init {
campaignsService.campaignFlow campaignsService.campaignFlow
.onEach { campaignId -> resolveStartNavigation(campaignIdConverter.convert(campaignId)) } .onEach { request ->
resolveStartNavigation(
campaignType = campaignIdConverter.convert(request.campaignId),
userWalletId = request.userWalletId,
)
}
.launchIn(modelScope) .launchIn(modelScope)
} }
@Suppress("UnusedPrivateMember") private fun resolveStartNavigation(campaignType: CampaignType?, userWalletId: UserWalletId) {
private fun resolveStartNavigation(campaignType: CampaignType?) { modelScope.launch {
val config = when (campaignType) { val config = if (campaignType == null) {
is CampaignType.ReactivationCashback -> checkReactivationCashbackCampaignState(campaignType) CampaignsBottomSheetConfig.NotActive
is CampaignType.WhaleSwapCashback -> checkWhaleSwapCashbackCampaignState(campaignType) } else {
null -> CampaignsBottomSheetConfig.NotActive checkCampaignState(campaignType, userWalletId)
}
config?.let { bottomSheetNavigation.activate(it) }
} }
bottomSheetNavigation.activate(config)
} }
// TODO private suspend fun checkCampaignState(
private fun checkReactivationCashbackCampaignState(campaignType: CampaignType): CampaignsBottomSheetConfig { campaignType: CampaignType,
return CampaignsBottomSheetConfig.Activate(campaignType) userWalletId: UserWalletId,
} ): CampaignsBottomSheetConfig? = getPromoCampaignStateUseCase.invoke(
campaign = campaignType.toPromoCampaignId(),
userWalletId = userWalletId,
).fold(
ifLeft = { error ->
TangemLogger.e("Error getting campaign ${campaignType.campaignId} state", error)
messageSender.send(SnackbarMessage(message = resourceReference(R.string.common_unknown_error)))
null
},
ifRight = { campaignState ->
when (campaignState) {
is PromoCampaignState.Enrolled,
is PromoCampaignState.Available,
-> CampaignsBottomSheetConfig.Activate(campaignType)
is PromoCampaignState.NotActive -> CampaignsBottomSheetConfig.NotActive
}
},
)
// TODO fun onFooterExtraHeightReady(height: Dp) {
private fun checkWhaleSwapCashbackCampaignState(campaignType: CampaignType): CampaignsBottomSheetConfig { footerExtraHeightState.value = height
return CampaignsBottomSheetConfig.Activate(campaignType)
} }
fun onDismiss() { fun onDismiss() {
@ -58,22 +97,12 @@ internal class CampaignsModel @Inject constructor(
} }
fun onActivated(campaignType: CampaignType) { fun onActivated(campaignType: CampaignType) {
footerExtraHeightState.value = 0.dp
bottomSheetNavigation.activate(CampaignsBottomSheetConfig.Enrolled(campaignType)) bottomSheetNavigation.activate(CampaignsBottomSheetConfig.Enrolled(campaignType))
} }
fun onAlreadyActivated( fun onAlreadyActivated(campaignType: CampaignType) {
campaignType: CampaignType, footerExtraHeightState.value = 0.dp
appCurrency: AppCurrency, bottomSheetNavigation.activate(CampaignsBottomSheetConfig.AlreadyActivated(campaignType = campaignType))
account: Account?,
currency: CryptoCurrencyStatus,
) {
bottomSheetNavigation.activate(
CampaignsBottomSheetConfig.AlreadyActivated(
campaignType = campaignType,
appCurrency = appCurrency,
account = account,
currency = currency,
),
)
} }
} }

View file

@ -1,5 +1,6 @@
package com.tangem.features.promobanners.impl.campaigns.service package com.tangem.features.promobanners.impl.campaigns.service
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
/** /**
@ -9,9 +10,15 @@ import kotlinx.coroutines.flow.Flow
*/ */
internal interface CampaignsService { internal interface CampaignsService {
/** Emits the campaignId requested via [show]. */ /** Emits the campaign requested via [show]. */
val campaignFlow: Flow<String> val campaignFlow: Flow<CampaignRequest>
/** Requests showing the campaign identified by [campaignId]. */ /** Requests showing the campaign identified by [campaignId] for the given [userWalletId]. */
fun show(campaignId: String) fun show(campaignId: String, userWalletId: UserWalletId)
} }
/** Payload of the campaigns bus: the campaign id and the wallet the campaign should be activated for. */
internal data class CampaignRequest(
val campaignId: String,
val userWalletId: UserWalletId,
)

View file

@ -1,5 +1,6 @@
package com.tangem.features.promobanners.impl.campaigns.service package com.tangem.features.promobanners.impl.campaigns.service
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
@ -9,10 +10,10 @@ import javax.inject.Singleton
@Singleton @Singleton
internal class DefaultCampaignsService @Inject constructor() : CampaignsService { internal class DefaultCampaignsService @Inject constructor() : CampaignsService {
private val _campaignFlow: Channel<String> = Channel(Channel.BUFFERED) private val _campaignFlow: Channel<CampaignRequest> = Channel(Channel.BUFFERED)
override val campaignFlow: Flow<String> = _campaignFlow.receiveAsFlow() override val campaignFlow: Flow<CampaignRequest> = _campaignFlow.receiveAsFlow()
override fun show(campaignId: String) { override fun show(campaignId: String, userWalletId: UserWalletId) {
_campaignFlow.trySend(campaignId) _campaignFlow.trySend(CampaignRequest(campaignId = campaignId, userWalletId = userWalletId))
} }
} }

View file

@ -1,7 +1,6 @@
package com.tangem.features.promobanners.impl.campaigns.ui package com.tangem.features.promobanners.impl.campaigns.ui
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -12,7 +11,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -21,7 +19,9 @@ import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.extensions.resolveReference 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.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.promobanners.impl.R import com.tangem.features.promobanners.impl.R
@ -29,21 +29,20 @@ import com.tangem.features.promobanners.impl.campaigns.entity.ActivateCampaignUM
import com.tangem.features.promobanners.impl.campaigns.entity.SelectedAccountUM import com.tangem.features.promobanners.impl.campaigns.entity.SelectedAccountUM
@Composable @Composable
internal fun ActivateCampaignContent(um: ActivateCampaignUM) { internal fun ActivateCampaignContent(um: ActivateCampaignUM, modifier: Modifier = Modifier) {
Column( Column(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Image( TangemIcon(
// TODO([REDACTED_TASK_KEY]): replace placeholder with the real campaign illustration. tangemIconUM = um.logo,
painter = painterResource(R.drawable.ill_businessman_3d),
contentDescription = null,
modifier = Modifier modifier = Modifier
.size(80.dp) .size(80.dp)
.clip(CircleShape), .clip(CircleShape),
) )
SpacerH32() SpacerH32()
Text( Text(
@ -66,18 +65,18 @@ internal fun ActivateCampaignContent(um: ActivateCampaignUM) {
SpacerH12() SpacerH12()
Text( Text(
// TODO([REDACTED_TASK_KEY]): localize text = stringResourceSafe(R.string.common_learn_more),
text = "Learn more",
style = TangemTheme.typography3.caption.medium, style = TangemTheme.typography3.caption.medium,
color = TangemTheme.colors3.text.primary, color = TangemTheme.colors3.text.primary,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .align(Alignment.Start)
.clickable(onClick = um.onLearnMoreClick), .clickable(onClick = um.onLearnMoreClick),
) )
SelectedTokenContent( SelectedTokenContent(
selectedToken = um.selectedToken, selectedToken = um.selectedToken,
selectedAccount = um.selectedAccount, selectedAccount = um.selectedAccount,
onChooseTokenClick = um.onChooseTokenClick,
) )
SpacerH32() SpacerH32()
@ -85,12 +84,16 @@ internal fun ActivateCampaignContent(um: ActivateCampaignUM) {
} }
@Composable @Composable
private fun SelectedTokenContent(selectedToken: TokenItemState?, selectedAccount: SelectedAccountUM?) { private fun SelectedTokenContent(
selectedToken: TokenItemState?,
selectedAccount: SelectedAccountUM?,
onChooseTokenClick: () -> Unit,
) {
if (selectedToken != null) { if (selectedToken != null) {
SpacerH24() SpacerH24()
Text( Text(
text = "Select cashback account", // TODO([REDACTED_TASK_KEY]): localize text = stringResourceSafe(R.string.promo_campaign_select_cashback_account),
style = TangemTheme.typography.subtitle1, style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1, color = TangemTheme.colors.text.primary1,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -102,7 +105,10 @@ private fun SelectedTokenContent(selectedToken: TokenItemState?, selectedAccount
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(24.dp)) .clip(RoundedCornerShape(24.dp))
.background(TangemTheme.colors.background.primary), .background(TangemTheme.colors3.bg.tertiary)
.clickable {
onChooseTokenClick.invoke()
},
selectedToken = selectedToken, selectedToken = selectedToken,
selectedAccount = selectedAccount, selectedAccount = selectedAccount,
) )

View file

@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
@ -16,6 +18,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH12
@ -26,17 +29,28 @@ import com.tangem.features.promobanners.impl.campaigns.entity.FooterUM
import com.tangem.features.promobanners.impl.campaigns.entity.TermsUM import com.tangem.features.promobanners.impl.campaigns.entity.TermsUM
@Composable @Composable
internal fun ActivateCampaignFooter(footerUM: FooterUM, modifier: Modifier = Modifier) { internal fun ActivateCampaignFooter(
footerUM: FooterUM,
onFooterTextHeightReady: (Dp) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) { Column(modifier = modifier) {
val terms = footerUM.terms val terms = footerUM.terms
if (terms != null) { if (terms != null) {
val density = LocalDensity.current
Text( Text(
text = termsAnnotatedString(terms), text = termsAnnotatedString(terms),
style = TangemTheme.typography.caption2, style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary, color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.onSizeChanged {
val termsBlockHeight = with(density) { it.height.toDp() } + 12.dp
onFooterTextHeightReady.invoke(termsBlockHeight)
},
) )
SpacerH12() SpacerH12()
@ -85,6 +99,7 @@ private fun Preview_ActivateCampaignFooter_WithTerms() {
modifier = Modifier modifier = Modifier
.background(TangemTheme.colors3.bg.primary) .background(TangemTheme.colors3.bg.primary)
.padding(16.dp), .padding(16.dp),
onFooterTextHeightReady = {},
) )
} }
} }
@ -99,6 +114,7 @@ private fun Preview_ActivateCampaignFooter_NoTerms() {
modifier = Modifier modifier = Modifier
.background(TangemTheme.colors3.bg.primary) .background(TangemTheme.colors3.bg.primary)
.padding(16.dp), .padding(16.dp),
onFooterTextHeightReady = {},
) )
} }
} }

View file

@ -13,21 +13,19 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_info_24 import com.tangem.core.ui.res.generated.icons.ic_info_24
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignAlreadyActivatedUM
import com.tangem.features.promobanners.impl.campaigns.entity.SelectedAccountUM
@Composable @Composable
internal fun ActivateCampaignContent(um: CampaignAlreadyActivatedUM) { internal fun AlreadyActivatedCampaignContent(message: TextReference, modifier: Modifier = Modifier) {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Box( Box(
@ -48,7 +46,7 @@ internal fun ActivateCampaignContent(um: CampaignAlreadyActivatedUM) {
SpacerH32() SpacerH32()
Text( Text(
text = "Youre already enrolled in Whale Swap Cashback", // TODO localization text = message.resolveReference(),
style = TangemTheme.typography3.heading.small, style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary, color = TangemTheme.colors3.text.primary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -57,39 +55,10 @@ internal fun ActivateCampaignContent(um: CampaignAlreadyActivatedUM) {
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
) )
SpacerH12()
SelectedTokenContent(
selectedToken = um.selectedToken,
selectedAccount = um.selectedAccount,
)
SpacerH32() SpacerH32()
} }
} }
@Composable
private fun SelectedTokenContent(selectedToken: TokenItemState, selectedAccount: SelectedAccountUM?) {
SpacerH24()
Text(
text = "Eligible cashback will be distributed to:", // TODO([REDACTED_TASK_KEY]): localize
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
SpacerH12()
PromoCampaignTokenItem(
modifier = Modifier.fillMaxWidth(),
selectedToken = selectedToken,
selectedAccount = selectedAccount,
)
}
// region Preview // region Preview
@Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@ -97,7 +66,7 @@ private fun SelectedTokenContent(selectedToken: TokenItemState, selectedAccount:
private fun Preview_AlreadyActivatedCampaignContent() { private fun Preview_AlreadyActivatedCampaignContent() {
TangemThemePreviewRedesign { TangemThemePreviewRedesign {
Box(modifier = Modifier.background(TangemTheme.colors3.bg.primary)) { Box(modifier = Modifier.background(TangemTheme.colors3.bg.primary)) {
ActivateCampaignContent(um = CampaignPreviewData.alreadyActivated) AlreadyActivatedCampaignContent(message = stringReference("Youre already enrolled in Whale Swap Cashback"))
} }
} }
} }

View file

@ -1,5 +1,6 @@
package com.tangem.features.promobanners.impl.campaigns.ui package com.tangem.features.promobanners.impl.campaigns.ui
import android.content.res.Configuration
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -14,12 +15,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_success_24 import com.tangem.core.ui.res.generated.icons.ic_success_24
@ -56,6 +59,21 @@ fun CampaignEnrolledMessageContent(message: TextReference, modifier: Modifier =
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
SpacerH(48.dp) SpacerH32()
} }
} }
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_CampaignEnrolledMessageContent() {
TangemThemePreviewRedesign {
Box(modifier = Modifier.background(TangemTheme.colors3.bg.primary)) {
CampaignEnrolledMessageContent(
message = stringReference("Youre successfully enrolled in Enroll in Whale Swap Cashback"),
)
}
}
}
// endregion

View file

@ -3,9 +3,10 @@ package com.tangem.features.promobanners.impl.campaigns.ui
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.promobanners.impl.R
import com.tangem.features.promobanners.impl.campaigns.entity.ActivateCampaignUM import com.tangem.features.promobanners.impl.campaigns.entity.ActivateCampaignUM
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignAlreadyActivatedUM
import com.tangem.features.promobanners.impl.campaigns.entity.FooterUM import com.tangem.features.promobanners.impl.campaigns.entity.FooterUM
import com.tangem.features.promobanners.impl.campaigns.entity.SelectedAccountUM import com.tangem.features.promobanners.impl.campaigns.entity.SelectedAccountUM
import com.tangem.features.promobanners.impl.campaigns.entity.TermsUM import com.tangem.features.promobanners.impl.campaigns.entity.TermsUM
@ -51,6 +52,7 @@ internal object CampaignPreviewData {
) )
val activateCampaign: ActivateCampaignUM = ActivateCampaignUM( val activateCampaign: ActivateCampaignUM = ActivateCampaignUM(
logo = TangemIconUM.Icon(R.drawable.ic_alert_24),
title = stringReference("Whale Swap Cashback"), title = stringReference("Whale Swap Cashback"),
description = stringReference( description = stringReference(
"Get cashback on every swap. Pick a token and the account where your rewards will be paid out.", "Get cashback on every swap. Pick a token and the account where your rewards will be paid out.",
@ -61,10 +63,6 @@ internal object CampaignPreviewData {
footerUM = footer, footerUM = footer,
onChooseTokenDismiss = {}, onChooseTokenDismiss = {},
onLearnMoreClick = {}, onLearnMoreClick = {},
) onChooseTokenClick = {},
val alreadyActivated: CampaignAlreadyActivatedUM = CampaignAlreadyActivatedUM(
selectedToken = tokenItem,
selectedAccount = selectedAccount,
) )
} }

View file

@ -15,9 +15,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH import com.tangem.features.promobanners.impl.R
import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_warning_24 import com.tangem.core.ui.res.generated.icons.ic_warning_24
@ -48,7 +49,7 @@ fun NotActiveCampaignMessageContent(modifier: Modifier = Modifier) {
SpacerH32() SpacerH32()
Text( Text(
text = " Campaign not active", // TODO localization text = stringResourceSafe(R.string.promo_campaign_not_active_title),
style = TangemTheme.typography3.heading.small, style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary, color = TangemTheme.colors3.text.primary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -58,13 +59,13 @@ fun NotActiveCampaignMessageContent(modifier: Modifier = Modifier) {
SpacerH8() SpacerH8()
Text( Text(
text = "This campaign no longer exists or has expired", // TODO localization text = stringResourceSafe(R.string.promo_campaign_not_active_subtitle),
style = TangemTheme.typography3.subheading.medium, style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.secondary, color = TangemTheme.colors3.text.secondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
SpacerH(48.dp) SpacerH32()
} }
} }