From 370d902dad4120f830054ba0b3ddb88d0ab5a828 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 13:31:41 +0200 Subject: [PATCH] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 10 +++++ .../com/tangem/tap/routing/RootContent.kt | 3 ++ .../component/impl/DefaultRoutingComponent.kt | 8 ++++ .../tap/routing/utils/DeepLinkFactory.kt | 3 ++ .../tap/routing/utils/DeepLinkFactoryTest.kt | 6 +++ .../tangem/common/routing/DeepLinkRoute.kt | 4 ++ .../common/routing/deeplink/DeeplinkConst.kt | 1 + .../api/deeplink/CampaignsDeepLinkHandler.kt | 8 ++++ .../DefaultCampaignsDeepLinkHandler.kt | 37 +++++++++++++++++++ .../impl/campaigns/di/CampaignsModule.kt | 14 +++++++ .../impl/campaigns/model/CampaignsModel.kt | 14 ++++--- .../campaigns/service/CampaignsService.kt | 17 +++++++++ .../service/DefaultCampaignsService.kt | 18 +++++++++ 13 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 features/promo-banners/api/src/main/kotlin/com/tangem/features/promobanners/api/deeplink/CampaignsDeepLinkHandler.kt create mode 100644 features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/deeplink/DefaultCampaignsDeepLinkHandler.kt create mode 100644 features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/service/CampaignsService.kt create mode 100644 features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/campaigns/service/DefaultCampaignsService.kt 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