Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-07 12:07:08 +02:00
parent 576332d67d
commit a2c8e7e95a
21 changed files with 755 additions and 0 deletions

View file

@ -13,4 +13,5 @@ dependencies {
api(deps.compose.ui)
api(projects.core.decompose)
api(projects.core.ui)
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.promobanners.api.swapcashback
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
/**
* once at app startup (hence [Unit] params) and reacts to campaign requests coming through the
* promo-campaigns bus, not to navigation.
*/
interface CampaignsComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Unit, CampaignsComponent>
}

View file

@ -0,0 +1,6 @@
package com.tangem.features.promobanners.api.toggles
interface PromoBannersFeatureToggles {
val isCampaignsToggleEnabled: Boolean
}

View file

@ -1,6 +1,7 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
@ -13,12 +14,17 @@ android {
dependencies {
/** Project - API */
api(projects.features.promoBanners.api)
implementation(projects.features.commonFeatures.api)
implementation(projects.common.routing)
implementation(projects.common.ui)
/** Domain */
api(projects.domain.common)
implementation(projects.domain.models)
implementation(projects.domain.appCurrency)
/** Core */
api(projects.core.configToggles)
api(projects.core.analytics)
api(projects.core.datasource)
api(projects.core.decompose)
@ -37,6 +43,7 @@ dependencies {
implementation(deps.androidx.appCompat)
implementation(deps.androidx.core.ktx)
implementation(deps.decompose)
implementation(deps.decompose.ext.compose)
implementation(deps.kotlin.coroutines)
implementation(deps.kotlin.immutable.collections)

View file

@ -0,0 +1,64 @@
package com.tangem.features.promobanners.impl.campaigns.component
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
import com.tangem.features.promobanners.impl.campaigns.model.ActivateCampaignsModel
import com.tangem.features.promobanners.impl.campaigns.ui.ActivateCampaignContent
internal class ActivateCampaignBottomSheetComponent(
appComponentContext: AppComponentContext,
chooseTokenComponentFactory: ChooseTokenComponent.Factory,
params: ActivateCampaignsModel.Params,
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
private val model: ActivateCampaignsModel = getOrCreateModel(params)
private val chooseTokenComponent: ChooseTokenComponent = chooseTokenComponentFactory.create(
context = child(key = "swapCashbackChooseToken"),
params = ChooseTokenComponent.Params(bridge = model.bridge),
)
override fun dismiss() = model.onDismiss()
@Composable
override fun BottomSheet() {
val state by model.uiState.collectAsStateWithLifecycle()
ActivateCampaignContent(
state = state,
onSelectTokenClick = model::onSelectTokenClick,
onEnrollClick = model::onEnrollClick,
onLearnMoreClick = { /* [REDACTED_TODO_COMMENT] */ },
onDismiss = ::dismiss,
)
if (state.isChoosingToken) {
ChooseTokenBottomSheet()
}
}
@Composable
private fun ChooseTokenBottomSheet() {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = model::onChooseTokenDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = model::onChooseTokenDismiss,
) {
chooseTokenComponent.Content(modifier = Modifier.fillMaxWidth())
}
}
}

View file

@ -0,0 +1,50 @@
package com.tangem.features.promobanners.impl.campaigns.component
import androidx.compose.runtime.Composable
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
import com.tangem.core.ui.components.bottomsheets.message.messageBottomSheetUM
import com.tangem.core.ui.components.bottomsheets.message.onClick
import com.tangem.core.ui.components.bottomsheets.message.onDismiss
import com.tangem.core.ui.components.bottomsheets.message.primaryButton
import com.tangem.core.ui.components.bottomsheets.message.vector
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_success_24
import com.tangem.features.promobanners.impl.R
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
import com.tangem.features.promobanners.impl.campaigns.entity.campaignName
internal class CampaignEnrolledBottomSheetComponent(
private val campaignType: CampaignType,
private val onDismissRequest: () -> Unit,
) : ComposableBottomSheetComponent {
override fun dismiss() = onDismissRequest()
@Composable
override fun BottomSheet() {
MessageBottomSheet(
state = messageBottomSheetUM {
onDismiss(onDismissRequest)
infoBlock {
vector(Icons.ic_success_24) {
type = MessageBottomSheetUM.Vector.Type.Accent
backgroundType = MessageBottomSheetUM.Vector.BackgroundType.SameAsTint
}
title = stringReference("You're successfully enrolled in ${campaignType.campaignName()}")
body = stringReference("Your cashback will be applied to eligible swaps automatically.")
}
primaryButton {
text = resourceReference(R.string.common_close)
onClick { closeBs() }
}
},
onDismissRequest = onDismissRequest,
)
}
}

View file

@ -0,0 +1,72 @@
package com.tangem.features.promobanners.impl.campaigns.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
import com.tangem.features.promobanners.api.swapcashback.CampaignsComponent
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignsBottomSheetConfig
import com.tangem.features.promobanners.impl.campaigns.model.ActivateCampaignsModel
import com.tangem.features.promobanners.impl.campaigns.model.CampaignsModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultCampaignsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
private val chooseTokenComponentFactory: ChooseTokenComponent.Factory,
) : CampaignsComponent, AppComponentContext by appComponentContext {
private val model: CampaignsModel = getOrCreateModel()
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = CampaignsBottomSheetConfig.serializer(),
handleBackButton = false,
childFactory = ::bottomSheetChild,
)
@Composable
override fun Content(modifier: Modifier) {
val bottomSheet by bottomSheetSlot.subscribeAsState()
bottomSheet.child?.instance?.BottomSheet()
}
private fun bottomSheetChild(
config: CampaignsBottomSheetConfig,
componentContext: ComponentContext,
): ComposableBottomSheetComponent {
val context = childByContext(componentContext)
return when (config) {
CampaignsBottomSheetConfig.NotActive -> NotActiveCampaignBottomSheetComponent(
onDismissRequest = model::onDismiss,
)
is CampaignsBottomSheetConfig.Enrolled -> CampaignEnrolledBottomSheetComponent(
campaignType = config.campaignType,
onDismissRequest = model::onDismiss,
)
is CampaignsBottomSheetConfig.Activate -> ActivateCampaignBottomSheetComponent(
appComponentContext = context,
chooseTokenComponentFactory = chooseTokenComponentFactory,
params = ActivateCampaignsModel.Params(
campaignType = config.campaignType,
onDismiss = model::onDismiss,
onActivated = model::onActivated,
),
)
}
}
@AssistedFactory
interface Factory : CampaignsComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultCampaignsComponent
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.features.promobanners.impl.campaigns.component
import androidx.compose.runtime.Composable
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM
import com.tangem.core.ui.components.bottomsheets.message.icon
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
import com.tangem.core.ui.components.bottomsheets.message.messageBottomSheetUM
import com.tangem.core.ui.components.bottomsheets.message.onClick
import com.tangem.core.ui.components.bottomsheets.message.onDismiss
import com.tangem.core.ui.components.bottomsheets.message.primaryButton
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.promobanners.impl.R
internal class NotActiveCampaignBottomSheetComponent(
private val onDismissRequest: () -> Unit,
) : ComposableBottomSheetComponent {
override fun dismiss() = onDismissRequest()
@Composable
override fun BottomSheet() {
MessageBottomSheet(
state = messageBottomSheetUM {
onDismiss(onDismissRequest)
infoBlock {
icon(R.drawable.ic_alert_circle_24) {
type = MessageBottomSheetUM.Icon.Type.Warning
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.SameAsTint
}
title = stringReference("Campaign not active") // TODO localization
body = stringReference("This campaign no longer exists or has expired.") // TODO localization
}
primaryButton {
text = resourceReference(R.string.common_close)
onClick { closeBs() }
}
},
onDismissRequest = onDismissRequest,
)
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.promobanners.impl.campaigns.converters
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
import com.tangem.utils.converter.Converter
import javax.inject.Inject
internal class CampaignIdConverter @Inject constructor() :
Converter<String, CampaignType?> {
override fun convert(value: String): CampaignType? {
return when (value) {
CAMPAIGN_ID_REACTIVATION -> CampaignType.ReactivationCashback(campaignId = value)
CAMPAIGN_ID_WHALE -> CampaignType.WhaleSwapCashback(campaignId = value)
else -> null
}
}
private companion object {
const val CAMPAIGN_ID_WHALE = "1"
const val CAMPAIGN_ID_REACTIVATION = "2"
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.features.promobanners.impl.campaigns.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.promobanners.api.swapcashback.CampaignsComponent
import com.tangem.features.promobanners.impl.campaigns.component.DefaultCampaignsComponent
import com.tangem.features.promobanners.impl.campaigns.model.ActivateCampaignsModel
import com.tangem.features.promobanners.impl.campaigns.model.CampaignsModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface CampaignsModule {
@Binds
@Singleton
fun bindCampaignsComponentFactory(factory: DefaultCampaignsComponent.Factory): CampaignsComponent.Factory
}
@Module
@InstallIn(ModelComponent::class)
internal interface CampaignsModelModule {
@Binds
@IntoMap
@ClassKey(CampaignsModel::class)
fun bindCampaignModel(model: CampaignsModel): Model
@Binds
@IntoMap
@ClassKey(ActivateCampaignsModel::class)
fun bindCampaignActivateModel(model: ActivateCampaignsModel): Model
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.promobanners.impl.campaigns.entity
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
/**
* State of the campaign activation bottom sheet.
*
* The intro promo screen (image + title + description) is always shown. When [selectedToken] is `null`
* the footer shows "Select token"; once a token is chosen it shows the account block, the terms agreement
* and the "Enroll" button. When [isChoosingToken] is `true` the token selector is shown on top of the
* intro (as a stacked bottom sheet), not instead of it.
*/
internal data class ActivateCampaignUM(
val campaignName: String,
val title: TextReference,
val description: TextReference,
val selectedToken: TokenItemState?,
val isChoosingToken: Boolean,
)

View file

@ -0,0 +1,12 @@
package com.tangem.features.promobanners.impl.campaigns.entity
import kotlinx.serialization.Serializable
@Serializable
sealed interface CampaignType {
val campaignId: String
data class ReactivationCashback(override val campaignId: String) : CampaignType
data class WhaleSwapCashback(override val campaignId: String) : CampaignType
}

View file

@ -0,0 +1,7 @@
package com.tangem.features.promobanners.impl.campaigns.entity
internal fun CampaignType.campaignName(): String = when (this) {
// TODO([REDACTED_TASK_KEY]): source real campaign display names.
is CampaignType.ReactivationCashback -> "Reactivation Cashback"
is CampaignType.WhaleSwapCashback -> "Whale Swap Cashback"
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.promobanners.impl.campaigns.entity
import kotlinx.serialization.Serializable
@Serializable
internal sealed class CampaignsBottomSheetConfig {
@Serializable
data object NotActive : CampaignsBottomSheetConfig()
@Serializable
data class Enrolled(
val campaignType: CampaignType,
) : CampaignsBottomSheetConfig()
@Serializable
data class Activate(
val campaignType: CampaignType,
) : CampaignsBottomSheetConfig()
}

View file

@ -0,0 +1,110 @@
package com.tangem.features.promobanners.impl.campaigns.model
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.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
import com.tangem.features.promobanners.impl.R
import com.tangem.features.promobanners.impl.campaigns.entity.ActivateCampaignUM
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
import com.tangem.features.promobanners.impl.campaigns.entity.campaignName
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@ModelScoped
internal class ActivateCampaignsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
chooseTokenBridgeFactory: ChooseTokenBridge.Factory,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
) : Model() {
private val params = paramsContainer.require<Params>()
private var appCurrency: AppCurrency = AppCurrency.Default
val uiState: StateFlow<ActivateCampaignUM>
field = MutableStateFlow(getInitialState())
val bridge: ChooseTokenBridge = chooseTokenBridgeFactory.create(
modelScope = modelScope,
settings = ChooseTokenBridge.Settings(
title = resourceReference(R.string.common_choose_token),
isShowMarketBlock = false,
isShowPaymentAccount = false,
isShowSingleCurrencyWallets = true,
),
)
init {
getSelectedAppCurrencyUseCase.invokeOrDefault()
.onEach { appCurrency = it }
.launchIn(modelScope)
bridge.onCurrencyChosen.receiveAsFlow()
.onEach { result -> onTokenChosen(result) }
.launchIn(modelScope)
bridge.onClose.receiveAsFlow()
.onEach { onChooseTokenDismiss() }
.launchIn(modelScope)
}
fun onSelectTokenClick() {
uiState.update { it.copy(isChoosingToken = true) }
}
fun onChooseTokenDismiss() {
uiState.update { it.copy(isChoosingToken = false) }
}
fun onEnrollClick() {
// TODO([REDACTED_TASK_KEY]): call the real campaign enrollment use case with the chosen token before proceeding.
params.onActivated(params.campaignType)
}
fun onDismiss() = params.onDismiss()
private fun onTokenChosen(result: ChooseTokenResult) {
val tokenItem = TokenItemStateConverter(appCurrency = appCurrency).convert(result.currency)
uiState.update { state ->
state.copy(
isChoosingToken = false,
selectedToken = tokenItem,
)
}
}
private fun getInitialState(): ActivateCampaignUM {
return ActivateCampaignUM(
campaignName = params.campaignType.campaignName(),
// 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.\n\n",
),
selectedToken = null,
isChoosingToken = false,
)
}
data class Params(
val campaignType: CampaignType,
val onDismiss: () -> Unit,
val onActivated: (CampaignType) -> Unit,
)
}

View file

@ -0,0 +1,56 @@
package com.tangem.features.promobanners.impl.campaigns.model
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignsBottomSheetConfig
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import javax.inject.Inject
@ModelScoped
internal class CampaignsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
// private val campaignIdConverter: CampaignIdConverter,
// campaignsService: CampaignsService,
) : Model() {
val bottomSheetNavigation: SlotNavigation<CampaignsBottomSheetConfig> = SlotNavigation()
init {
// campaignsService.campaignFlow
// .onEach { campaignId -> resolveStartNavigation(campaignIdConverter.convert(campaignId)) }
// .launchIn(modelScope)
}
@Suppress("UnusedPrivateMember")
private fun resolveStartNavigation(campaignType: CampaignType?) {
val config = when (campaignType) {
is CampaignType.ReactivationCashback -> checkReactivationCashbackCampaignState(campaignType)
is CampaignType.WhaleSwapCashback -> checkWhaleSwapCashbackCampaignState(campaignType)
null -> CampaignsBottomSheetConfig.NotActive
}
bottomSheetNavigation.activate(config)
}
// TODO
private fun checkReactivationCashbackCampaignState(campaignType: CampaignType): CampaignsBottomSheetConfig {
return CampaignsBottomSheetConfig.Activate(campaignType)
}
// TODO
private fun checkWhaleSwapCashbackCampaignState(campaignType: CampaignType): CampaignsBottomSheetConfig {
return CampaignsBottomSheetConfig.Activate(campaignType)
}
fun onDismiss() {
bottomSheetNavigation.dismiss()
}
fun onActivated(campaignType: CampaignType) {
bottomSheetNavigation.activate(CampaignsBottomSheetConfig.Enrolled(campaignType))
}
}

View file

@ -0,0 +1,50 @@
package com.tangem.features.promobanners.impl.campaigns.ui
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.runtime.Composable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.ds2.button.Close
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.promobanners.impl.campaigns.entity.ActivateCampaignUM
@Composable
internal fun ActivateCampaignContent(
state: ActivateCampaignUM,
onSelectTokenClick: () -> Unit,
onEnrollClick: () -> Unit,
onLearnMoreClick: () -> Unit,
onDismiss: () -> Unit,
) {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
containerColor = TangemTheme.colors.background.tertiary,
onBack = onDismiss,
title = {
TangemTopNavigation(
windowInsets = WindowInsets(0),
blurBackground = false,
endButton = {
TangemButton.Close(
onClick = onDismiss,
)
},
)
},
content = {
ActivateCampaignContent(
state = state,
onSelectTokenClick = onSelectTokenClick,
onEnrollClick = onEnrollClick,
onLearnMoreClick = onLearnMoreClick,
)
},
)
}

View file

@ -0,0 +1,135 @@
package com.tangem.features.promobanners.impl.campaigns.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.promobanners.impl.R
import com.tangem.features.promobanners.impl.campaigns.entity.ActivateCampaignUM
@Composable
internal fun ActivateCampaignContent(
state: ActivateCampaignUM,
onSelectTokenClick: () -> Unit,
onEnrollClick: () -> Unit,
onLearnMoreClick: () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16)
.navigationBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
// TODO([REDACTED_TASK_KEY]): replace placeholder with the real campaign illustration.
painter = painterResource(R.drawable.ill_businessman_3d),
contentDescription = null,
modifier = Modifier
.size(TangemTheme.dimens.size96)
.clip(CircleShape),
)
SpacerH16()
Text(
text = state.title.resolveReference(),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth(),
)
SpacerH8()
Text(
text = state.description.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier.fillMaxWidth(),
)
SpacerH12()
Text(
// TODO([REDACTED_TASK_KEY]): localize
text = "Learn more",
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.accent,
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onLearnMoreClick),
)
val selectedToken = state.selectedToken
if (selectedToken != null) {
SpacerH24()
Text(
text = "Select cashback account", // TODO([REDACTED_TASK_KEY]): localize
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.fillMaxWidth(),
)
SpacerH12()
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
.background(TangemTheme.colors.background.primary)
.padding(vertical = TangemTheme.dimens.spacing12),
) {
TokenItem(state = selectedToken, isBalanceHidden = false)
}
}
SpacerH24()
Footer(
campaignName = state.campaignName,
hasSelectedToken = selectedToken != null,
onSelectTokenClick = onSelectTokenClick,
onEnrollClick = onEnrollClick,
)
}
}
@Composable
private fun Footer(
campaignName: String,
hasSelectedToken: Boolean,
onSelectTokenClick: () -> Unit,
onEnrollClick: () -> Unit,
) {
if (hasSelectedToken) {
Text(
text = "I agree with $campaignName Terms", // TODO([REDACTED_TASK_KEY]): localize + clickable terms
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
SpacerH12()
}
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = if (hasSelectedToken) {
"Enroll" // TODO([REDACTED_TASK_KEY]): localize
} else {
"Select token" // TODO([REDACTED_TASK_KEY]): localize
},
onClick = if (hasSelectedToken) onEnrollClick else onSelectTokenClick,
)
SpacerH16()
}

View file

@ -5,10 +5,12 @@ import com.tangem.core.decompose.model.Model
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import com.tangem.features.promobanners.api.toggles.PromoBannersFeatureToggles
import com.tangem.features.promobanners.impl.DefaultPromoBannersBlockComponent
import com.tangem.features.promobanners.impl.model.PromoBannersBlockModel
import com.tangem.features.promobanners.impl.repository.DefaultPromoBannersRepository
import com.tangem.features.promobanners.impl.repository.PromoBannersRepository
import com.tangem.features.promobanners.impl.toggles.DefaultPromoBannersFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Binds
import dagger.Module
@ -29,6 +31,10 @@ internal interface PromoBannersFeatureModule {
factory: DefaultPromoBannersBlockComponent.Factory,
): PromoBannersBlockComponent.Factory
@Binds
@Singleton
fun bindPromoBannersFeatureToggles(impl: DefaultPromoBannersFeatureToggles): PromoBannersFeatureToggles
companion object {
@Provides

View file

@ -0,0 +1,16 @@
package com.tangem.features.promobanners.impl.toggles
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.promobanners.api.toggles.PromoBannersFeatureToggles
import javax.inject.Inject
class DefaultPromoBannersFeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) : PromoBannersFeatureToggles {
override val isCampaignsToggleEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
FeatureToggles.TWI_1637_CASHBACK_AND_REACTIVATION_CAMPAIGNS_ENABLED,
)
}