diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 71831c5102..43dc8567c8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -347,6 +347,16 @@
android:host="yield"
android:scheme="tangem" />
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/tangem/tap/routing/RootContent.kt b/app/src/main/java/com/tangem/tap/routing/RootContent.kt
index 279a41730a..16c2e9db89 100644
--- a/app/src/main/java/com/tangem/tap/routing/RootContent.kt
+++ b/app/src/main/java/com/tangem/tap/routing/RootContent.kt
@@ -42,6 +42,7 @@ internal fun RootContent(
onBack: () -> Unit,
modifier: Modifier = Modifier,
wcContent: @Composable (modifier: Modifier) -> Unit,
+ promoContent: @Composable (modifier: Modifier) -> Unit,
hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit,
startupGateContent: @Composable (modifier: Modifier) -> Unit,
scanFailsContent: @Composable (modifier: Modifier) -> Unit,
@@ -80,6 +81,8 @@ internal fun RootContent(
wcContent(Modifier.fillMaxSize())
+ promoContent(Modifier.fillMaxSize())
+
hotAccessCodeContent(Modifier.fillMaxSize())
startupGateContent(Modifier.fillMaxSize())
diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt
index 1b27cfa27a..fe36c2853d 100644
--- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt
+++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt
@@ -44,6 +44,7 @@ import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
+import com.tangem.features.promobanners.api.swapcashback.CampaignsComponent
import com.tangem.features.walletconnect.components.WcRoutingComponent
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.android.create
@@ -77,6 +78,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val appRouterConfig: AppRouterConfig,
private val uiDependencies: UiDependencies,
private val wcRoutingComponentFactory: WcRoutingComponent.Factory,
+ private val campaignsComponentFactory: CampaignsComponent.Factory,
private val deeplinkFactory: DeepLinkFactory,
private val tangemHotSDKProxy: TangemHotSDKProxy,
private val hotAccessCodeRequestComponentFactory: HotAccessCodeRequestComponent.Factory,
@@ -105,6 +107,11 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
.create(child("wcRoutingComponent"), params = Unit)
}
+ private val campaignsComponent: CampaignsComponent by lazy {
+ campaignsComponentFactory
+ .create(child("swapCashbackCampaign"), params = Unit)
+ }
+
private val hotAccessCodeRequestComponent: HotAccessCodeRequestComponent by lazy {
hotAccessCodeRequestComponentFactory
.create(child("hotAccessCodeRequestComponent"), Unit)
@@ -234,6 +241,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
onBack = router::pop,
modifier = modifier,
wcContent = { wcRoutingComponent.Content(it) },
+ promoContent = { campaignsComponent.Content(it) },
hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) },
startupGateContent = { appStartupGateComponent.Content(it) },
scanFailsContent = { scanFailsComponent.Content(it) },
diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
index c30d377259..397a018e74 100644
--- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
+++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
@@ -17,6 +17,7 @@ import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler
+import com.tangem.features.promobanners.api.deeplink.CampaignsDeepLinkHandler
import com.tangem.features.send.api.deeplink.SellRedirectDeepLinkHandler
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
import com.tangem.features.survey.deeplink.SurveyDeepLinkHandler
@@ -66,6 +67,7 @@ internal class DeepLinkFactory @Inject constructor(
private val earnDeepLink: EarnDeepLinkHandler.Factory,
private val yieldDeepLink: YieldDeepLinkHandler.Factory,
private val surveyDeepLink: SurveyDeepLinkHandler.Factory,
+ private val promoCampaignsDeepLink: CampaignsDeepLinkHandler.Factory,
) {
private val permittedAppRoute = MutableStateFlow(false)
@@ -181,6 +183,7 @@ internal class DeepLinkFactory @Inject constructor(
DeepLinkRoute.Yield.host -> yieldDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.PayAppMain.host -> tangemPayMainDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.Survey.host -> surveyDeepLink.create(queryParams)
+ DeepLinkRoute.Campaigns.host -> promoCampaignsDeepLink.create(queryParams)
else -> {
TangemLogger.i(
"""
diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
index c6744e1b77..0f6292f7fd 100644
--- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
+++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
@@ -16,6 +16,7 @@ import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler
+import com.tangem.features.promobanners.api.deeplink.CampaignsDeepLinkHandler
import com.tangem.features.send.api.deeplink.SellRedirectDeepLinkHandler
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler
@@ -117,6 +118,10 @@ class DeepLinkFactoryTest {
every { create(any(), any()) } returns mockk()
}
+ private val campaignsDeepLinkHandlerFactory = mockk(relaxed = true) {
+ every { create(any()) } returns mockk()
+ }
+
private val marketsTokenExchangesDeepLinkFactory =
mockk(relaxed = true) {
every { create(any(), any()) } returns mockk()
@@ -152,6 +157,7 @@ class DeepLinkFactoryTest {
earnDeepLink = earnDeepLinkFactory,
yieldDeepLink = yieldDeepLinkFactory,
surveyDeepLink = surveyDeepLinkFactory,
+ promoCampaignsDeepLink = campaignsDeepLinkHandlerFactory,
)
@OptIn(ExperimentalCoroutinesApi::class)
diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt
index 9abdcf8b09..8127bcb689 100644
--- a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt
+++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt
@@ -95,6 +95,10 @@ sealed class DeepLinkRoute {
data object Survey : DeepLinkRoute() {
override val host: String = "survey"
}
+
+ data object Campaigns : DeepLinkRoute() {
+ override val host: String = "campaigns"
+ }
}
enum class DeepLinkScheme(val scheme: String) {
diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt
index 677f35d658..167bd3bec2 100644
--- a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt
+++ b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt
@@ -16,6 +16,7 @@ object DeeplinkConst {
const val PROMO_CODE_KEY = "promo_code"
const val REF_KEY = "ref"
const val CAMPAIGN_KEY = "campaign"
+ const val CAMPAIGN_ID_KEY = "campaignId"
const val NAME_KEY = "name"
const val ORDER_KEY = "order"
const val INTERVAL_KEY = "interval"
diff --git a/features/promo-banners/api/src/main/kotlin/com/tangem/features/promobanners/api/deeplink/CampaignsDeepLinkHandler.kt b/features/promo-banners/api/src/main/kotlin/com/tangem/features/promobanners/api/deeplink/CampaignsDeepLinkHandler.kt
new file mode 100644
index 0000000000..3304191034
--- /dev/null
+++ b/features/promo-banners/api/src/main/kotlin/com/tangem/features/promobanners/api/deeplink/CampaignsDeepLinkHandler.kt
@@ -0,0 +1,8 @@
+package com.tangem.features.promobanners.api.deeplink
+
+interface CampaignsDeepLinkHandler {
+
+ interface Factory {
+ fun create(queryParams: Map): CampaignsDeepLinkHandler
+ }
+}
\ No newline at end of file
diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/deeplink/DefaultCampaignsDeepLinkHandler.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/deeplink/DefaultCampaignsDeepLinkHandler.kt
new file mode 100644
index 0000000000..4d9c7cdac9
--- /dev/null
+++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/deeplink/DefaultCampaignsDeepLinkHandler.kt
@@ -0,0 +1,37 @@
+package com.tangem.features.promobanners.impl.campaigns.deeplink
+
+import com.tangem.common.routing.deeplink.DeeplinkConst
+import com.tangem.features.promobanners.api.deeplink.CampaignsDeepLinkHandler
+import com.tangem.features.promobanners.api.toggles.PromoBannersFeatureToggles
+import com.tangem.features.promobanners.impl.campaigns.service.CampaignsService
+import com.tangem.utils.logging.TangemLogger
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/**
+ * Handles the campaigns deeplink (`tangem://campaigns?campaignId=1&lang=ru`): extracts the `campaignId` and
+ * pushes it to the promo-campaigns bus. The always-alive [com.tangem.features.promobanners.api.swapcashback
+ * .SwapCashbackCampaignComponent] listens to the bus, maps the id to a campaign type, resolves the campaign
+ * state and shows the right sheet over the current screen.
+ */
+internal class DefaultCampaignsDeepLinkHandler @AssistedInject constructor(
+ @Assisted private val queryParams: Map,
+ campaignsService: CampaignsService,
+ private val promoBannersFeatureToggles: PromoBannersFeatureToggles,
+) : CampaignsDeepLinkHandler {
+
+ init {
+ if (promoBannersFeatureToggles.isCampaignsToggleEnabled) {
+ val campaignId = queryParams[DeeplinkConst.CAMPAIGN_ID_KEY].orEmpty()
+ campaignsService.show(campaignId)
+ } else {
+ TangemLogger.i("Campaigns feature is disabled")
+ }
+ }
+
+ @AssistedFactory
+ interface Factory : CampaignsDeepLinkHandler.Factory {
+ override fun create(queryParams: Map): DefaultCampaignsDeepLinkHandler
+ }
+}
\ No newline at end of file
diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/di/CampaignsModule.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/di/CampaignsModule.kt
index bc1085694b..a3ad2a34b4 100644
--- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/di/CampaignsModule.kt
+++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/di/CampaignsModule.kt
@@ -2,10 +2,14 @@ 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.deeplink.CampaignsDeepLinkHandler
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.CampaignsModel
+import com.tangem.features.promobanners.impl.campaigns.service.CampaignsService
+import com.tangem.features.promobanners.impl.campaigns.service.DefaultCampaignsService
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -21,6 +25,16 @@ internal interface CampaignsModule {
@Binds
@Singleton
fun bindCampaignsComponentFactory(factory: DefaultCampaignsComponent.Factory): CampaignsComponent.Factory
+
+ @Binds
+ @Singleton
+ fun bindCampaignsDeepLinkHandlerFactory(
+ factory: DefaultCampaignsDeepLinkHandler.Factory,
+ ): CampaignsDeepLinkHandler.Factory
+
+ @Binds
+ @Singleton
+ fun bindCampaignsService(service: DefaultCampaignsService): CampaignsService
}
@Module
diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/model/CampaignsModel.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/model/CampaignsModel.kt
index d0d49dc073..ab5bba4e11 100644
--- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/model/CampaignsModel.kt
+++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/model/CampaignsModel.kt
@@ -5,24 +5,28 @@ 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.converters.CampaignIdConverter
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignsBottomSheetConfig
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
+import com.tangem.features.promobanners.impl.campaigns.service.CampaignsService
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
@ModelScoped
internal class CampaignsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
- // private val campaignIdConverter: CampaignIdConverter,
- // campaignsService: CampaignsService,
+ private val campaignIdConverter: CampaignIdConverter,
+ campaignsService: CampaignsService,
) : Model() {
val bottomSheetNavigation: SlotNavigation = SlotNavigation()
init {
- // campaignsService.campaignFlow
- // .onEach { campaignId -> resolveStartNavigation(campaignIdConverter.convert(campaignId)) }
- // .launchIn(modelScope)
+ campaignsService.campaignFlow
+ .onEach { campaignId -> resolveStartNavigation(campaignIdConverter.convert(campaignId)) }
+ .launchIn(modelScope)
}
@Suppress("UnusedPrivateMember")
diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/service/CampaignsService.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/service/CampaignsService.kt
new file mode 100644
index 0000000000..a05d5ccbb7
--- /dev/null
+++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/service/CampaignsService.kt
@@ -0,0 +1,17 @@
+package com.tangem.features.promobanners.impl.campaigns.service
+
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * App-wide bus that decouples the promo-campaigns deeplink handler from the UI that shows the campaign
+ * bottom sheet. A producer (deeplink handler) calls [show]; the always-alive campaign component listens
+ * to [campaignFlow] and activates the appropriate sheet over the current screen.
+ */
+internal interface CampaignsService {
+
+ /** Emits the campaignId requested via [show]. */
+ val campaignFlow: Flow
+
+ /** Requests showing the campaign identified by [campaignId]. */
+ fun show(campaignId: String)
+}
\ No newline at end of file
diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/service/DefaultCampaignsService.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/service/DefaultCampaignsService.kt
new file mode 100644
index 0000000000..cc51f81c6f
--- /dev/null
+++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/service/DefaultCampaignsService.kt
@@ -0,0 +1,18 @@
+package com.tangem.features.promobanners.impl.campaigns.service
+
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.receiveAsFlow
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+internal class DefaultCampaignsService @Inject constructor() : CampaignsService {
+
+ private val _campaignFlow: Channel = Channel(Channel.BUFFERED)
+ override val campaignFlow: Flow = _campaignFlow.receiveAsFlow()
+
+ override fun show(campaignId: String) {
+ _campaignFlow.trySend(campaignId)
+ }
+}
\ No newline at end of file