Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-07 16:37:19 +02:00
parent 8af068b74b
commit d8f696186e
25 changed files with 1127 additions and 235 deletions

View file

@ -49,11 +49,11 @@ data class MessageBottomSheetUM(
var backgroundType: BackgroundType = BackgroundType.Unspecified,
) : Element {
enum class Type {
Unspecified, Accent, Informative, Attention, Warning,
Unspecified, Accent, Informative, Attention, Warning, Success,
}
enum class BackgroundType {
Unspecified, SameAsTint, Accent, Informative, Attention, Warning,
Unspecified, SameAsTint, Accent, Informative, Attention, Warning, Success,
}
}

View file

@ -214,6 +214,7 @@ private fun BottomSheetVector(vector: MessageBottomSheetUM.Vector, modifier: Mod
MessageBottomSheetUM.Vector.Type.Informative -> TangemTheme.colors3.icon.status.info
MessageBottomSheetUM.Vector.Type.Attention -> TangemTheme.colors3.icon.status.warning
MessageBottomSheetUM.Vector.Type.Warning -> TangemTheme.colors3.icon.status.error
MessageBottomSheetUM.Vector.Type.Success -> TangemTheme.colors3.icon.status.success
}
val backgroundColor = when (vector.backgroundType) {
@ -223,6 +224,7 @@ private fun BottomSheetVector(vector: MessageBottomSheetUM.Vector, modifier: Mod
MessageBottomSheetUM.Vector.BackgroundType.Informative -> TangemTheme.colors3.bg.status.infoSubtle
MessageBottomSheetUM.Vector.BackgroundType.Attention -> TangemTheme.colors3.bg.status.warningSubtle
MessageBottomSheetUM.Vector.BackgroundType.Warning -> TangemTheme.colors3.bg.status.errorSubtle
MessageBottomSheetUM.Vector.BackgroundType.Success -> TangemTheme.colors3.bg.status.successSubtle
}
Box(

View file

@ -22,6 +22,7 @@ dependencies {
api(projects.domain.common)
implementation(projects.domain.models)
implementation(projects.domain.appCurrency)
implementation(projects.domain.account.status)
/** Core */
api(projects.core.configToggles)
@ -54,4 +55,5 @@ dependencies {
/** Tests */
testImplementation(deps.test.junit5)
testImplementation(deps.test.truth)
testImplementation(deps.kotlin.serialization)
}

View file

@ -1,26 +1,33 @@
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.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
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.TangemBottomSheet
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.core.ui.components.bottomsheets.state.BottomSheetState
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.features.commonfeatures.api.choosetoken.ChooseTokenComponent
import com.tangem.features.promobanners.impl.campaigns.model.ActivateCampaignsModel
import com.tangem.features.promobanners.impl.campaigns.ui.ActivateCampaignContent
import com.tangem.features.promobanners.impl.campaigns.ui.ActivateCampaignFooter
internal class ActivateCampaignBottomSheetComponent(
appComponentContext: AppComponentContext,
chooseTokenComponentFactory: ChooseTokenComponent.Factory,
params: ActivateCampaignsModel.Params,
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
private val params: ActivateCampaignsModel.Params,
) : CampaignsModularComponent, AppComponentContext by appComponentContext {
private val model: ActivateCampaignsModel = getOrCreateModel(params)
@ -29,36 +36,45 @@ internal class ActivateCampaignBottomSheetComponent(
params = ChooseTokenComponent.Params(bridge = model.bridge),
)
override fun dismiss() = model.onDismiss()
@Composable
override fun Title(bottomSheetState: State<BottomSheetState>) {
TangemTopNavigation(
windowInsets = WindowInsets(0),
blurBackground = false,
endButton = { TangemButton.Close(onClick = params.onDismiss) },
)
}
@Composable
override fun BottomSheet() {
override fun Content(bottomSheetState: State<BottomSheetState>, contentPadding: PaddingValues, modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
ActivateCampaignContent(
state = state,
onSelectTokenClick = model::onSelectTokenClick,
onEnrollClick = model::onEnrollClick,
onLearnMoreClick = { /* [REDACTED_TODO_COMMENT] */ },
onDismiss = ::dismiss,
)
ActivateCampaignContent(um = state)
if (state.isChoosingToken) {
ChooseTokenBottomSheet()
ChooseTokenBottomSheet(state.onChooseTokenDismiss)
}
}
@Composable
private fun ChooseTokenBottomSheet() {
override fun Footer() {
val state by model.uiState.collectAsStateWithLifecycle()
ActivateCampaignFooter(
footerUM = state.footerUM,
)
}
@Composable
private fun ChooseTokenBottomSheet(onChooseTokenDismiss: () -> Unit) {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = model::onChooseTokenDismiss,
onDismissRequest = onChooseTokenDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = model::onChooseTokenDismiss,
) {
chooseTokenComponent.Content(modifier = Modifier.fillMaxWidth())
}
onBack = onChooseTokenDismiss,
content = { chooseTokenComponent.Content(modifier = Modifier.fillMaxWidth()) },
)
}
}

View file

@ -0,0 +1,54 @@
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.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
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.model.getOrCreateModel
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
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.extensions.stringResourceSafe
import com.tangem.features.promobanners.impl.R
import com.tangem.features.promobanners.impl.campaigns.model.CampaignAlreadyActivatedModel
import com.tangem.features.promobanners.impl.campaigns.ui.ActivateCampaignContent
internal class CampaignAlreadyActivatedBottomSheetComponent(
appComponentContext: AppComponentContext,
private val params: CampaignAlreadyActivatedModel.Params,
) : CampaignsModularComponent, AppComponentContext by appComponentContext {
private val model: CampaignAlreadyActivatedModel = getOrCreateModel(params)
@Composable
override fun Title(bottomSheetState: State<BottomSheetState>) {
TangemTopNavigation(
windowInsets = WindowInsets(0),
blurBackground = false,
endButton = { TangemButton.Close(onClick = params.onDismiss) },
)
}
@Composable
override fun Content(bottomSheetState: State<BottomSheetState>, contentPadding: PaddingValues, modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
ActivateCampaignContent(um = state)
}
@Composable
override fun Footer() {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(R.string.common_close),
onClick = params.onDismiss,
)
}
}

View file

@ -1,50 +1,50 @@
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.fillMaxWidth
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 androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
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.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.core.ui.extensions.stringResourceSafe
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
import com.tangem.features.promobanners.impl.campaigns.ui.CampaignEnrolledMessageContent
internal class CampaignEnrolledBottomSheetComponent(
private val campaignType: CampaignType,
private val onDismissRequest: () -> Unit,
) : ComposableBottomSheetComponent {
override fun dismiss() = onDismissRequest()
) : CampaignsModularComponent {
@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
override fun Title(bottomSheetState: State<BottomSheetState>) {
TangemTopNavigation(
windowInsets = WindowInsets(0),
blurBackground = false,
endButton = { TangemButton.Close(onClick = onDismissRequest) },
)
}
title = stringReference("You're successfully enrolled in ${campaignType.campaignName()}")
body = stringReference("Your cashback will be applied to eligible swaps automatically.")
@Composable
override fun Content(bottomSheetState: State<BottomSheetState>, contentPadding: PaddingValues, modifier: Modifier) {
CampaignEnrolledMessageContent(
message = stringReference("You're successfully enrolled in ${campaignType.campaignName()}"),
)
}
primaryButton {
text = resourceReference(R.string.common_close)
onClick { closeBs() }
}
},
onDismissRequest = onDismissRequest,
@Composable
override fun Footer() {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(R.string.common_close),
onClick = { onDismissRequest() },
)
}
}

View file

@ -0,0 +1,10 @@
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

@ -1,19 +1,36 @@
package com.tangem.features.promobanners.impl.campaigns.component
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
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.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.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.extensions.rememberLastNonNull
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.CampaignAlreadyActivatedModel
import com.tangem.features.promobanners.impl.campaigns.model.CampaignsModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -29,7 +46,7 @@ internal class DefaultCampaignsComponent @AssistedInject constructor(
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = CampaignsBottomSheetConfig.serializer(),
serializer = null,
handleBackButton = false,
childFactory = ::bottomSheetChild,
)
@ -37,13 +54,61 @@ internal class DefaultCampaignsComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val bottomSheet by bottomSheetSlot.subscribeAsState()
bottomSheet.child?.instance?.BottomSheet()
val activeChild = bottomSheet.child?.instance
val displayedChild = rememberLastNonNull(activeChild)
val bottomSheetState = remember { mutableStateOf(BottomSheetState.EXPANDED) }
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = activeChild != null,
onDismissRequest = model::onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = model::onDismiss,
title = {
displayedChild?.Title(bottomSheetState)
},
content = {
val bottomInset = LocalTangemBottomSheetContentBottomInset.current
val scrollableSignal = LocalBottomSheetContentScrollable.current
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 = {
Box(
modifier = Modifier
.navigationBarsPadding()
.padding(12.dp),
) {
displayedChild?.Footer()
}
},
)
}
private fun bottomSheetChild(
config: CampaignsBottomSheetConfig,
componentContext: ComponentContext,
): ComposableBottomSheetComponent {
): CampaignsModularComponent {
val context = childByContext(componentContext)
return when (config) {
CampaignsBottomSheetConfig.NotActive -> NotActiveCampaignBottomSheetComponent(
@ -60,6 +125,24 @@ internal class DefaultCampaignsComponent @AssistedInject constructor(
campaignType = config.campaignType,
onDismiss = model::onDismiss,
onActivated = model::onActivated,
onAlreadyActivated = { campaignType, appCurrency, account, currency ->
model.onAlreadyActivated(
campaignType = campaignType,
appCurrency = appCurrency,
account = account,
currency = currency,
)
},
),
)
is CampaignsBottomSheetConfig.AlreadyActivated -> CampaignAlreadyActivatedBottomSheetComponent(
appComponentContext = context,
params = CampaignAlreadyActivatedModel.Params(
campaignType = config.campaignType,
appCurrency = config.appCurrency,
account = config.account,
currency = config.currency,
onDismiss = model::onDismiss,
),
)
}

View file

@ -1,44 +1,44 @@
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.fillMaxWidth
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 androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
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.extensions.stringResourceSafe
import com.tangem.features.promobanners.impl.R
import com.tangem.features.promobanners.impl.campaigns.ui.NotActiveCampaignMessageContent
internal class NotActiveCampaignBottomSheetComponent(
private val onDismissRequest: () -> Unit,
) : ComposableBottomSheetComponent {
override fun dismiss() = onDismissRequest()
) : CampaignsModularComponent {
@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
override fun Title(bottomSheetState: State<BottomSheetState>) {
TangemTopNavigation(
windowInsets = WindowInsets(0),
blurBackground = false,
endButton = { TangemButton.Close(onClick = onDismissRequest) },
)
}
title = stringReference("Campaign not active") // TODO localization
body = stringReference("This campaign no longer exists or has expired.") // TODO localization
@Composable
override fun Content(bottomSheetState: State<BottomSheetState>, contentPadding: PaddingValues, modifier: Modifier) {
NotActiveCampaignMessageContent()
}
primaryButton {
text = resourceReference(R.string.common_close)
onClick { closeBs() }
}
},
onDismissRequest = onDismissRequest,
@Composable
override fun Footer() {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(R.string.common_close),
onClick = { onDismissRequest() },
)
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.features.promobanners.api.swapcashback.CampaignsComponent
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.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.service.CampaignsService
import com.tangem.features.promobanners.impl.campaigns.service.DefaultCampaignsService
@ -50,4 +51,9 @@ internal interface CampaignsModelModule {
@IntoMap
@ClassKey(ActivateCampaignsModel::class)
fun bindCampaignActivateModel(model: ActivateCampaignsModel): Model
@Binds
@IntoMap
@ClassKey(CampaignAlreadyActivatedModel::class)
fun bindCampaignAlreadyActivatedModel(model: CampaignAlreadyActivatedModel): Model
}

View file

@ -1,5 +1,7 @@
package com.tangem.features.promobanners.impl.campaigns.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
@ -10,11 +12,39 @@ import com.tangem.core.ui.extensions.TextReference
* 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.
*
* [selectedAccount] is shown above the token only in accounts (multi-account) mode it names the account
* the asset was picked from. It is `null` in single-account mode or before a token is chosen.
*/
@Immutable
internal data class ActivateCampaignUM(
val campaignName: String,
val title: TextReference,
val description: TextReference,
val selectedToken: TokenItemState?,
val selectedAccount: SelectedAccountUM?,
val isChoosingToken: Boolean,
val footerUM: FooterUM,
val onChooseTokenDismiss: () -> Unit,
val onLearnMoreClick: () -> Unit,
)
@Immutable
internal data class FooterUM(
val label: TextReference,
val onPrimaryButtonClick: () -> Unit,
val terms: TermsUM? = null,
)
@Immutable
data class TermsUM(
val text: TextReference,
val linkText: TextReference,
val onTermsClick: () -> Unit,
)
@Immutable
internal data class SelectedAccountUM(
val iconState: CurrencyIconState,
val name: TextReference,
)

View file

@ -0,0 +1,10 @@
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

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

View file

@ -1,5 +1,8 @@
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
@Serializable
@ -17,4 +20,12 @@ internal sealed class CampaignsBottomSheetConfig {
data class Activate(
val campaignType: CampaignType,
) : CampaignsBottomSheetConfig()
@Serializable
data class AlreadyActivated(
val campaignType: CampaignType,
val appCurrency: AppCurrency,
val account: Account?,
val currency: CryptoCurrencyStatus,
) : CampaignsBottomSheetConfig()
}

View file

@ -1,18 +1,29 @@
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.navigation.url.UrlOpener
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
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.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.FooterUM
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.campaignName
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
@ -21,6 +32,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ModelScoped
@ -29,11 +41,15 @@ internal class ActivateCampaignsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
chooseTokenBridgeFactory: ChooseTokenBridge.Factory,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val urlOpener: UrlOpener,
) : Model() {
private val params = paramsContainer.require<Params>()
private val accountIconConverter = AccountIconItemStateConverter(size = AccountIconSize.ExtraSmall)
private var appCurrency: AppCurrency = AppCurrency.Default
private var selectedAccount: Account? = null
private var selectedCurrency: CryptoCurrencyStatus? = null
val uiState: StateFlow<ActivateCampaignUM>
field = MutableStateFlow(getInitialState())
@ -62,43 +78,104 @@ internal class ActivateCampaignsModel @Inject constructor(
.launchIn(modelScope)
}
fun onSelectTokenClick() {
private fun onSelectTokenClick() {
uiState.update { it.copy(isChoosingToken = true) }
}
fun onChooseTokenDismiss() {
private 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.
private fun onEnrollClick() {
modelScope.launch {
// TODO([REDACTED_TASK_KEY]): call the real campaign enrollment use case; its result decides the next sheet.
// Success -> the "enrolled" sheet; "already activated" error -> hand the chosen token/account
// over to the "already activated" sheet.
if (enrollInCampaign()) {
params.onActivated(params.campaignType)
} else {
selectedCurrency?.let {
params.onAlreadyActivated(params.campaignType, appCurrency, selectedAccount, it)
}
}
}
}
fun onDismiss() = params.onDismiss()
@Suppress("FunctionOnlyReturningConstant") // TODO([REDACTED_TASK_KEY]): stub until the real enrollment use case exists.
private fun enrollInCampaign(): Boolean {
// TODO([REDACTED_TASK_KEY]): replace with the real enrollment use case call; `true` = enrolled, `false` = already active.
return true
}
private fun onTermsClick() {
urlOpener.openUrl(CAMPAIGN_TERMS_URL)
}
private fun onLearnMoreClick() {
urlOpener.openUrl(CAMPAIGN_TERMS_URL)
}
private fun onTokenChosen(result: ChooseTokenResult) {
modelScope.launch {
val selectedAccountUM = if (isAccountsModeEnabledUseCase.invokeSync()) {
val account = result.account.account
selectedAccount = 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
}
} else {
null
}
selectedCurrency = result.currency
val tokenItem = TokenItemStateConverter(appCurrency = appCurrency).convert(result.currency)
uiState.update { state ->
state.copy(
isChoosingToken = false,
selectedToken = tokenItem,
selectedAccount = selectedAccountUM,
footerUM = FooterUM(
label = stringReference("Enroll"),
onPrimaryButtonClick = ::onEnrollClick,
terms = TermsUM(
// TODO([REDACTED_TASK_KEY]): localize
text = stringReference("I agree with"),
linkText = stringReference("${params.campaignType.campaignName()} Terms"),
onTermsClick = ::onTermsClick,
),
),
)
}
}
}
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",
"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,
)
}
@ -106,5 +183,11 @@ internal class ActivateCampaignsModel @Inject constructor(
val campaignType: CampaignType,
val onDismiss: () -> Unit,
val onActivated: (CampaignType) -> Unit,
val onAlreadyActivated: (CampaignType, AppCurrency, Account?, CryptoCurrencyStatus) -> Unit,
)
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

@ -0,0 +1,73 @@
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.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignAlreadyActivatedUM
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
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<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,
)
}
data class Params(
val campaignType: CampaignType,
val account: Account?,
val appCurrency: AppCurrency,
val currency: CryptoCurrencyStatus,
val onDismiss: () -> Unit,
)
}

View file

@ -5,6 +5,9 @@ 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.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
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.CampaignType
@ -57,4 +60,20 @@ internal class CampaignsModel @Inject constructor(
fun onActivated(campaignType: CampaignType) {
bottomSheetNavigation.activate(CampaignsBottomSheetConfig.Enrolled(campaignType))
}
fun onAlreadyActivated(
campaignType: CampaignType,
appCurrency: AppCurrency,
account: Account?,
currency: CryptoCurrencyStatus,
) {
bottomSheetNavigation.activate(
CampaignsBottomSheetConfig.AlreadyActivated(
campaignType = campaignType,
appCurrency = appCurrency,
account = account,
currency = currency,
),
)
}
}

View file

@ -1,50 +0,0 @@
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

@ -1,5 +1,6 @@
package com.tangem.features.promobanners.impl.campaigns.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -13,29 +14,26 @@ 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 androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
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.SpacerH32
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.promobanners.impl.R
import com.tangem.features.promobanners.impl.campaigns.entity.ActivateCampaignUM
import com.tangem.features.promobanners.impl.campaigns.entity.SelectedAccountUM
@Composable
internal fun ActivateCampaignContent(
state: ActivateCampaignUM,
onSelectTokenClick: () -> Unit,
onEnrollClick: () -> Unit,
onLearnMoreClick: () -> Unit,
) {
internal fun ActivateCampaignContent(um: ActivateCampaignUM) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16)
.navigationBarsPadding(),
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
@ -43,93 +41,99 @@ internal fun ActivateCampaignContent(
painter = painterResource(R.drawable.ill_businessman_3d),
contentDescription = null,
modifier = Modifier
.size(TangemTheme.dimens.size96)
.size(80.dp)
.clip(CircleShape),
)
SpacerH16()
SpacerH32()
Text(
text = state.title.resolveReference(),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
text = um.title.resolveReference(),
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary,
textAlign = TextAlign.Start,
modifier = Modifier.fillMaxWidth(),
)
SpacerH8()
Text(
text = state.description.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
text = um.description.resolveReference(),
style = TangemTheme.typography3.caption.medium,
color = TangemTheme.colors3.text.secondary,
modifier = Modifier.fillMaxWidth(),
)
SpacerH12()
Text(
// TODO([REDACTED_TASK_KEY]): localize
text = "Learn more",
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.accent,
style = TangemTheme.typography3.caption.medium,
color = TangemTheme.colors3.text.primary,
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onLearnMoreClick),
.clickable(onClick = um.onLearnMoreClick),
)
val selectedToken = state.selectedToken
SelectedTokenContent(
selectedToken = um.selectedToken,
selectedAccount = um.selectedAccount,
)
SpacerH32()
}
}
@Composable
private fun SelectedTokenContent(selectedToken: TokenItemState?, selectedAccount: SelectedAccountUM?) {
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(
PromoCampaignTokenItem(
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,
.clip(RoundedCornerShape(24.dp))
.background(TangemTheme.colors.background.primary),
selectedToken = selectedToken,
selectedAccount = selectedAccount,
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@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()
private fun Preview_ActivateCampaignContent_WithToken() {
TangemThemePreviewRedesign {
Box(modifier = Modifier.background(TangemTheme.colors3.bg.primary)) {
ActivateCampaignContent(um = CampaignPreviewData.activateCampaign)
}
}
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()
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ActivateCampaignContent_NoToken() {
TangemThemePreviewRedesign {
Box(modifier = Modifier.background(TangemTheme.colors3.bg.primary)) {
ActivateCampaignContent(
um = CampaignPreviewData.activateCampaign.copy(
selectedToken = null,
selectedAccount = null,
),
)
}
}
}
// endregion

View file

@ -0,0 +1,105 @@
package com.tangem.features.promobanners.impl.campaigns.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.promobanners.impl.campaigns.entity.FooterUM
import com.tangem.features.promobanners.impl.campaigns.entity.TermsUM
@Composable
internal fun ActivateCampaignFooter(footerUM: FooterUM, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
val terms = footerUM.terms
if (terms != null) {
Text(
text = termsAnnotatedString(terms),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
SpacerH12()
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = footerUM.label.resolveReference(),
onClick = footerUM.onPrimaryButtonClick,
)
}
}
@Composable
private fun termsAnnotatedString(terms: TermsUM) = buildAnnotatedString {
val startText = terms.text.resolveReference()
val linkText = terms.linkText.resolveReference()
append(startText)
append(" ")
withLink(
link = LinkAnnotation.Clickable(
tag = "CAMPAIGN_TERMS",
linkInteractionListener = { terms.onTermsClick() },
),
) {
withStyle(
SpanStyle(
color = TangemTheme.colors3.text.primary,
textDecoration = TextDecoration.None,
),
) {
append(linkText)
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ActivateCampaignFooter_WithTerms() {
TangemThemePreviewRedesign {
ActivateCampaignFooter(
footerUM = CampaignPreviewData.footer,
modifier = Modifier
.background(TangemTheme.colors3.bg.primary)
.padding(16.dp),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ActivateCampaignFooter_NoTerms() {
TangemThemePreviewRedesign {
ActivateCampaignFooter(
footerUM = CampaignPreviewData.footer.copy(terms = null),
modifier = Modifier
.background(TangemTheme.colors3.bg.primary)
.padding(16.dp),
)
}
}
// endregion

View file

@ -0,0 +1,104 @@
package com.tangem.features.promobanners.impl.campaigns.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
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.token.state.TokenItemState
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.ic_info_24
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignAlreadyActivatedUM
import com.tangem.features.promobanners.impl.campaigns.entity.SelectedAccountUM
@Composable
internal fun ActivateCampaignContent(um: CampaignAlreadyActivatedUM) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(TangemTheme.colors3.bg.status.infoSubtle),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(24.dp),
imageVector = Icons.ic_info_24,
contentDescription = null,
tint = TangemTheme.colors3.icon.status.info,
)
}
SpacerH32()
Text(
text = "Youre already enrolled in Whale Swap Cashback", // TODO localization
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
SpacerH12()
SelectedTokenContent(
selectedToken = um.selectedToken,
selectedAccount = um.selectedAccount,
)
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
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_AlreadyActivatedCampaignContent() {
TangemThemePreviewRedesign {
Box(modifier = Modifier.background(TangemTheme.colors3.bg.primary)) {
ActivateCampaignContent(um = CampaignPreviewData.alreadyActivated)
}
}
}
// endregion

View file

@ -0,0 +1,61 @@
package com.tangem.features.promobanners.impl.campaigns.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
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.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_success_24
@Composable
fun CampaignEnrolledMessageContent(message: TextReference, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(TangemTheme.colors3.bg.status.successSubtle),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(24.dp),
imageVector = Icons.ic_success_24,
contentDescription = null,
tint = TangemTheme.colors3.icon.status.success,
)
}
SpacerH32()
Text(
text = message.resolveReference(),
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
SpacerH(48.dp)
}
}

View file

@ -0,0 +1,70 @@
package com.tangem.features.promobanners.impl.campaigns.ui
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
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.SelectedAccountUM
import com.tangem.features.promobanners.impl.campaigns.entity.TermsUM
/**
* Shared preview fixtures for the campaign UI `@Preview`s. Not used in production code.
*/
internal object CampaignPreviewData {
val tokenItem: TokenItemState.Content = TokenItemState.Content(
id = "preview-token",
iconState = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = com.tangem.core.ui.R.drawable.img_polygon_22,
isGrayscale = false,
shouldShowCustomBadge = false,
),
titleState = TokenItemState.TitleState.Content(text = stringReference("Polygon")),
subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference("MATIC")),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $"),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "5,412 MATIC"),
onItemClick = {},
onItemLongClick = {},
)
val selectedAccount: SelectedAccountUM = SelectedAccountUM(
iconState = CurrencyIconState.CryptoPortfolio.Icon(
resId = com.tangem.core.ui.R.drawable.ic_rounded_star_24,
color = Color(color = 0xFF0099FF),
isGrayscale = false,
),
name = stringReference("Main account"),
)
val footer: FooterUM = FooterUM(
label = stringReference("Enroll"),
onPrimaryButtonClick = {},
terms = TermsUM(
text = stringReference("By enrolling you agree to the"),
linkText = stringReference("Terms & Conditions"),
onTermsClick = {},
),
)
val activateCampaign: ActivateCampaignUM = ActivateCampaignUM(
title = stringReference("Whale Swap Cashback"),
description = stringReference(
"Get cashback on every swap. Pick a token and the account where your rewards will be paid out.",
),
selectedToken = tokenItem,
selectedAccount = selectedAccount,
isChoosingToken = false,
footerUM = footer,
onChooseTokenDismiss = {},
onLearnMoreClick = {},
)
val alreadyActivated: CampaignAlreadyActivatedUM = CampaignAlreadyActivatedUM(
selectedToken = tokenItem,
selectedAccount = selectedAccount,
)
}

View file

@ -0,0 +1,70 @@
package com.tangem.features.promobanners.impl.campaigns.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
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.SpacerH8
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_warning_24
@Composable
fun NotActiveCampaignMessageContent(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(TangemTheme.colors3.bg.status.warningSubtle),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(24.dp),
imageVector = Icons.ic_warning_24,
contentDescription = null,
tint = TangemTheme.colors3.icon.status.warning,
)
}
SpacerH32()
Text(
text = " Campaign not active", // TODO localization
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
SpacerH8()
Text(
text = "This campaign no longer exists or has expired", // TODO localization
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
SpacerH(48.dp)
}
}

View file

@ -0,0 +1,126 @@
package com.tangem.features.promobanners.impl.campaigns.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.account.AccountCharIcon
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.account.AccountResIcon
import com.tangem.core.ui.components.account.PaymentAccountIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.promobanners.impl.campaigns.entity.SelectedAccountUM
@Composable
internal fun PromoCampaignTokenItem(
selectedToken: TokenItemState,
selectedAccount: SelectedAccountUM?,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
if (selectedAccount != null) {
Row(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
when (val icon = selectedAccount.iconState) {
is CurrencyIconState.PaymentAccount -> PaymentAccountIcon(size = icon.size)
is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon(
resId = icon.resId,
color = icon.color,
size = AccountIconSize.ExtraSmall,
)
is CurrencyIconState.CryptoPortfolio.Letter -> AccountCharIcon(
char = icon.char.resolveReference().first(),
color = icon.color,
size = AccountIconSize.ExtraSmall,
)
is CurrencyIconState.CoinIcon,
is CurrencyIconState.CustomTokenIcon,
is CurrencyIconState.Empty,
is CurrencyIconState.FiatIcon,
CurrencyIconState.Loading,
CurrencyIconState.Locked,
is CurrencyIconState.TokenIcon,
-> Unit
}
SpacerW4()
Text(
modifier = Modifier
.padding(vertical = 2.dp)
.alignByBaseline(),
text = selectedAccount.name.resolveReference(),
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.caption1,
)
}
SpacerH8()
}
TokenItem(
state = selectedToken,
isBalanceHidden = false,
itemPaddingValues = PaddingValues(horizontal = 16.dp),
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_PromoCampaignTokenItem_WithAccount() {
TangemThemePreviewRedesign {
PromoCampaignTokenItem(
modifier = Modifier
.background(TangemTheme.colors3.bg.primary)
.fillMaxWidth()
.clip(RoundedCornerShape(24.dp))
.background(TangemTheme.colors.background.primary),
selectedToken = CampaignPreviewData.tokenItem,
selectedAccount = CampaignPreviewData.selectedAccount,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_PromoCampaignTokenItem_NoAccount() {
TangemThemePreviewRedesign {
PromoCampaignTokenItem(
modifier = Modifier
.background(TangemTheme.colors3.bg.primary)
.fillMaxWidth()
.clip(RoundedCornerShape(24.dp))
.background(TangemTheme.colors.background.primary),
selectedToken = CampaignPreviewData.tokenItem,
selectedAccount = null,
)
}
}
// endregion