Updated on 2026-08-14
This commit is contained in:
parent
576332d67d
commit
a2c8e7e95a
21 changed files with 755 additions and 0 deletions
|
|
@ -162,5 +162,9 @@
|
||||||
{
|
{
|
||||||
"name": "TWI_1638_VA_MVP0_ENABLED",
|
"name": "TWI_1638_VA_MVP0_ENABLED",
|
||||||
"version": "6.1"
|
"version": "6.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TWI_1637_CASHBACK_AND_REACTIVATION_CAMPAIGNS_ENABLED",
|
||||||
|
"version": "undefined"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,5 @@ dependencies {
|
||||||
api(deps.compose.ui)
|
api(deps.compose.ui)
|
||||||
|
|
||||||
api(projects.core.decompose)
|
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 {
|
plugins {
|
||||||
alias(deps.plugins.android.library)
|
alias(deps.plugins.android.library)
|
||||||
alias(deps.plugins.kotlin.android)
|
alias(deps.plugins.kotlin.android)
|
||||||
|
alias(deps.plugins.kotlin.serialization)
|
||||||
alias(deps.plugins.kotlin.kapt)
|
alias(deps.plugins.kotlin.kapt)
|
||||||
alias(deps.plugins.hilt.android)
|
alias(deps.plugins.hilt.android)
|
||||||
id("configuration")
|
id("configuration")
|
||||||
|
|
@ -13,12 +14,17 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
/** Project - API */
|
/** Project - API */
|
||||||
api(projects.features.promoBanners.api)
|
api(projects.features.promoBanners.api)
|
||||||
|
implementation(projects.features.commonFeatures.api)
|
||||||
|
implementation(projects.common.routing)
|
||||||
|
implementation(projects.common.ui)
|
||||||
|
|
||||||
/** Domain */
|
/** Domain */
|
||||||
api(projects.domain.common)
|
api(projects.domain.common)
|
||||||
implementation(projects.domain.models)
|
implementation(projects.domain.models)
|
||||||
|
implementation(projects.domain.appCurrency)
|
||||||
|
|
||||||
/** Core */
|
/** Core */
|
||||||
|
api(projects.core.configToggles)
|
||||||
api(projects.core.analytics)
|
api(projects.core.analytics)
|
||||||
api(projects.core.datasource)
|
api(projects.core.datasource)
|
||||||
api(projects.core.decompose)
|
api(projects.core.decompose)
|
||||||
|
|
@ -37,6 +43,7 @@ dependencies {
|
||||||
implementation(deps.androidx.appCompat)
|
implementation(deps.androidx.appCompat)
|
||||||
implementation(deps.androidx.core.ktx)
|
implementation(deps.androidx.core.ktx)
|
||||||
implementation(deps.decompose)
|
implementation(deps.decompose)
|
||||||
|
implementation(deps.decompose.ext.compose)
|
||||||
implementation(deps.kotlin.coroutines)
|
implementation(deps.kotlin.coroutines)
|
||||||
implementation(deps.kotlin.immutable.collections)
|
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.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
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.DefaultPromoBannersBlockComponent
|
||||||
import com.tangem.features.promobanners.impl.model.PromoBannersBlockModel
|
import com.tangem.features.promobanners.impl.model.PromoBannersBlockModel
|
||||||
import com.tangem.features.promobanners.impl.repository.DefaultPromoBannersRepository
|
import com.tangem.features.promobanners.impl.repository.DefaultPromoBannersRepository
|
||||||
import com.tangem.features.promobanners.impl.repository.PromoBannersRepository
|
import com.tangem.features.promobanners.impl.repository.PromoBannersRepository
|
||||||
|
import com.tangem.features.promobanners.impl.toggles.DefaultPromoBannersFeatureToggles
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
|
@ -29,6 +31,10 @@ internal interface PromoBannersFeatureModule {
|
||||||
factory: DefaultPromoBannersBlockComponent.Factory,
|
factory: DefaultPromoBannersBlockComponent.Factory,
|
||||||
): PromoBannersBlockComponent.Factory
|
): PromoBannersBlockComponent.Factory
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
fun bindPromoBannersFeatureToggles(impl: DefaultPromoBannersFeatureToggles): PromoBannersFeatureToggles
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@Provides
|
@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