Updated on 2026-08-14
This commit is contained in:
parent
0de667c1d1
commit
2b928cfa9d
21 changed files with 772 additions and 10 deletions
|
|
@ -9,6 +9,9 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
api(deps.compose.runtime)
|
||||
api(deps.compose.ui)
|
||||
|
||||
api(projects.core.decompose)
|
||||
api(projects.core.ui)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.promobanners.api.toggles
|
||||
|
||||
interface PromoBannersFeatureToggles {
|
||||
|
||||
val isCampaignsToggleEnabled: Boolean
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
@ -12,28 +13,39 @@ android {
|
|||
|
||||
dependencies {
|
||||
/** Project - API */
|
||||
implementation(projects.features.promoBanners.api)
|
||||
api(projects.features.promoBanners.api)
|
||||
implementation(projects.features.commonFeatures.api)
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.common)
|
||||
api(projects.domain.common)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.appCurrency)
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.ui)
|
||||
api(projects.core.configToggles)
|
||||
api(projects.core.analytics)
|
||||
api(projects.core.datasource)
|
||||
api(projects.core.decompose)
|
||||
api(projects.core.navigation)
|
||||
api(projects.core.utils)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.foundation)
|
||||
api(deps.compose.foundation)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.lifecycle.compose)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.androidx.appCompat)
|
||||
implementation(deps.androidx.core.ktx)
|
||||
implementation(deps.decompose)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue