Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-07 13:31:41 +02:00
parent 2b928cfa9d
commit 7472e7b1dc
13 changed files with 138 additions and 5 deletions

View file

@ -347,6 +347,16 @@
android:host="yield" android:host="yield"
android:scheme="tangem" /> android:scheme="tangem" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="campaigns"
android:scheme="tangem" />
</intent-filter>
<intent-filter android:autoVerify="true"> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />

View file

@ -42,6 +42,7 @@ internal fun RootContent(
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
wcContent: @Composable (modifier: Modifier) -> Unit, wcContent: @Composable (modifier: Modifier) -> Unit,
promoContent: @Composable (modifier: Modifier) -> Unit,
hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit, hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit,
rootDetectedWarningContent: @Composable (modifier: Modifier) -> Unit, rootDetectedWarningContent: @Composable (modifier: Modifier) -> Unit,
scanFailsContent: @Composable (modifier: Modifier) -> Unit, scanFailsContent: @Composable (modifier: Modifier) -> Unit,
@ -80,6 +81,8 @@ internal fun RootContent(
wcContent(Modifier.fillMaxSize()) wcContent(Modifier.fillMaxSize())
promoContent(Modifier.fillMaxSize())
hotAccessCodeContent(Modifier.fillMaxSize()) hotAccessCodeContent(Modifier.fillMaxSize())
rootDetectedWarningContent(Modifier.fillMaxSize()) rootDetectedWarningContent(Modifier.fillMaxSize())

View file

@ -47,6 +47,7 @@ import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION 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.features.walletconnect.components.WcRoutingComponent
import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.android.create import com.tangem.hot.sdk.android.create
@ -83,6 +84,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val appRouterConfig: AppRouterConfig, private val appRouterConfig: AppRouterConfig,
private val uiDependencies: UiDependencies, private val uiDependencies: UiDependencies,
private val wcRoutingComponentFactory: WcRoutingComponent.Factory, private val wcRoutingComponentFactory: WcRoutingComponent.Factory,
private val campaignsComponentFactory: CampaignsComponent.Factory,
private val deeplinkFactory: DeepLinkFactory, private val deeplinkFactory: DeepLinkFactory,
private val tangemHotSDKProxy: TangemHotSDKProxy, private val tangemHotSDKProxy: TangemHotSDKProxy,
private val hotAccessCodeRequestComponentFactory: HotAccessCodeRequestComponent.Factory, private val hotAccessCodeRequestComponentFactory: HotAccessCodeRequestComponent.Factory,
@ -112,6 +114,11 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
.create(child("wcRoutingComponent"), params = Unit) .create(child("wcRoutingComponent"), params = Unit)
} }
private val campaignsComponent: CampaignsComponent by lazy {
campaignsComponentFactory
.create(child("swapCashbackCampaign"), params = Unit)
}
private val hotAccessCodeRequestComponent: HotAccessCodeRequestComponent by lazy { private val hotAccessCodeRequestComponent: HotAccessCodeRequestComponent by lazy {
hotAccessCodeRequestComponentFactory hotAccessCodeRequestComponentFactory
.create(child("hotAccessCodeRequestComponent"), Unit) .create(child("hotAccessCodeRequestComponent"), Unit)
@ -278,6 +285,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
onBack = router::pop, onBack = router::pop,
modifier = modifier, modifier = modifier,
wcContent = { wcRoutingComponent.Content(it) }, wcContent = { wcRoutingComponent.Content(it) },
promoContent = { campaignsComponent.Content(it) },
hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) }, hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) },
rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) }, rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) },
scanFailsContent = { scanFailsComponent.Content(it) }, scanFailsContent = { scanFailsComponent.Content(it) },

View file

@ -17,6 +17,7 @@ import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler 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.send.api.deeplink.SellRedirectDeepLinkHandler
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
import com.tangem.features.survey.deeplink.SurveyDeepLinkHandler import com.tangem.features.survey.deeplink.SurveyDeepLinkHandler
@ -66,6 +67,7 @@ internal class DeepLinkFactory @Inject constructor(
private val earnDeepLink: EarnDeepLinkHandler.Factory, private val earnDeepLink: EarnDeepLinkHandler.Factory,
private val yieldDeepLink: YieldDeepLinkHandler.Factory, private val yieldDeepLink: YieldDeepLinkHandler.Factory,
private val surveyDeepLink: SurveyDeepLinkHandler.Factory, private val surveyDeepLink: SurveyDeepLinkHandler.Factory,
private val promoCampaignsDeepLink: CampaignsDeepLinkHandler.Factory,
) { ) {
private val permittedAppRoute = MutableStateFlow(false) private val permittedAppRoute = MutableStateFlow(false)
@ -181,6 +183,7 @@ internal class DeepLinkFactory @Inject constructor(
DeepLinkRoute.Yield.host -> yieldDeepLink.create(coroutineScope, queryParams) DeepLinkRoute.Yield.host -> yieldDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.PayAppMain.host -> tangemPayMainDeepLink.create(coroutineScope, queryParams) DeepLinkRoute.PayAppMain.host -> tangemPayMainDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.Survey.host -> surveyDeepLink.create(queryParams) DeepLinkRoute.Survey.host -> surveyDeepLink.create(queryParams)
DeepLinkRoute.Campaigns.host -> promoCampaignsDeepLink.create(queryParams)
else -> { else -> {
TangemLogger.i( TangemLogger.i(
""" """

View file

@ -16,6 +16,7 @@ import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
import com.tangem.features.onramp.deeplink.SellDeepLinkHandler import com.tangem.features.onramp.deeplink.SellDeepLinkHandler
import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler 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.send.api.deeplink.SellRedirectDeepLinkHandler
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler
@ -117,6 +118,10 @@ class DeepLinkFactoryTest {
every { create(any(), any()) } returns mockk() every { create(any(), any()) } returns mockk()
} }
private val campaignsDeepLinkHandlerFactory = mockk<CampaignsDeepLinkHandler.Factory>(relaxed = true) {
every { create(any()) } returns mockk()
}
private val marketsTokenExchangesDeepLinkFactory = private val marketsTokenExchangesDeepLinkFactory =
mockk<MarketsTokenExchangesDeepLinkHandler.Factory>(relaxed = true) { mockk<MarketsTokenExchangesDeepLinkHandler.Factory>(relaxed = true) {
every { create(any(), any()) } returns mockk() every { create(any(), any()) } returns mockk()
@ -152,6 +157,7 @@ class DeepLinkFactoryTest {
earnDeepLink = earnDeepLinkFactory, earnDeepLink = earnDeepLinkFactory,
yieldDeepLink = yieldDeepLinkFactory, yieldDeepLink = yieldDeepLinkFactory,
surveyDeepLink = surveyDeepLinkFactory, surveyDeepLink = surveyDeepLinkFactory,
promoCampaignsDeepLink = campaignsDeepLinkHandlerFactory,
) )
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)

View file

@ -95,6 +95,10 @@ sealed class DeepLinkRoute {
data object Survey : DeepLinkRoute() { data object Survey : DeepLinkRoute() {
override val host: String = "survey" override val host: String = "survey"
} }
data object Campaigns : DeepLinkRoute() {
override val host: String = "campaigns"
}
} }
enum class DeepLinkScheme(val scheme: String) { enum class DeepLinkScheme(val scheme: String) {

View file

@ -16,6 +16,7 @@ object DeeplinkConst {
const val PROMO_CODE_KEY = "promo_code" const val PROMO_CODE_KEY = "promo_code"
const val REF_KEY = "ref" const val REF_KEY = "ref"
const val CAMPAIGN_KEY = "campaign" const val CAMPAIGN_KEY = "campaign"
const val CAMPAIGN_ID_KEY = "campaignId"
const val NAME_KEY = "name" const val NAME_KEY = "name"
const val ORDER_KEY = "order" const val ORDER_KEY = "order"
const val INTERVAL_KEY = "interval" const val INTERVAL_KEY = "interval"

View file

@ -0,0 +1,8 @@
package com.tangem.features.promobanners.api.deeplink
interface CampaignsDeepLinkHandler {
interface Factory {
fun create(queryParams: Map<String, String>): CampaignsDeepLinkHandler
}
}

View file

@ -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<String, String>,
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<String, String>): DefaultCampaignsDeepLinkHandler
}
}

View file

@ -2,10 +2,14 @@ package com.tangem.features.promobanners.impl.campaigns.di
import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model 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.api.swapcashback.CampaignsComponent
import com.tangem.features.promobanners.impl.campaigns.component.DefaultCampaignsComponent 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.ActivateCampaignsModel
import com.tangem.features.promobanners.impl.campaigns.model.CampaignsModel 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.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -21,6 +25,16 @@ internal interface CampaignsModule {
@Binds @Binds
@Singleton @Singleton
fun bindCampaignsComponentFactory(factory: DefaultCampaignsComponent.Factory): CampaignsComponent.Factory fun bindCampaignsComponentFactory(factory: DefaultCampaignsComponent.Factory): CampaignsComponent.Factory
@Binds
@Singleton
fun bindCampaignsDeepLinkHandlerFactory(
factory: DefaultCampaignsDeepLinkHandler.Factory,
): CampaignsDeepLinkHandler.Factory
@Binds
@Singleton
fun bindCampaignsService(service: DefaultCampaignsService): CampaignsService
} }
@Module @Module

View file

@ -5,24 +5,28 @@ import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model 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.CampaignsBottomSheetConfig
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType 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 com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject import javax.inject.Inject
@ModelScoped @ModelScoped
internal class CampaignsModel @Inject constructor( internal class CampaignsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
// private val campaignIdConverter: CampaignIdConverter, private val campaignIdConverter: CampaignIdConverter,
// campaignsService: CampaignsService, campaignsService: CampaignsService,
) : Model() { ) : Model() {
val bottomSheetNavigation: SlotNavigation<CampaignsBottomSheetConfig> = SlotNavigation() val bottomSheetNavigation: SlotNavigation<CampaignsBottomSheetConfig> = SlotNavigation()
init { init {
// campaignsService.campaignFlow campaignsService.campaignFlow
// .onEach { campaignId -> resolveStartNavigation(campaignIdConverter.convert(campaignId)) } .onEach { campaignId -> resolveStartNavigation(campaignIdConverter.convert(campaignId)) }
// .launchIn(modelScope) .launchIn(modelScope)
} }
@Suppress("UnusedPrivateMember") @Suppress("UnusedPrivateMember")

View file

@ -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<String>
/** Requests showing the campaign identified by [campaignId]. */
fun show(campaignId: String)
}

View file

@ -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<String> = Channel(Channel.BUFFERED)
override val campaignFlow: Flow<String> = _campaignFlow.receiveAsFlow()
override fun show(campaignId: String) {
_campaignFlow.trySend(campaignId)
}
}