Updated on 2026-08-14
This commit is contained in:
parent
2b928cfa9d
commit
7472e7b1dc
13 changed files with 138 additions and 5 deletions
|
|
@ -347,6 +347,16 @@
|
|||
android:host="yield"
|
||||
android:scheme="tangem" />
|
||||
</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">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
rootDetectedWarningContent: @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())
|
||||
|
||||
rootDetectedWarningContent(Modifier.fillMaxSize())
|
||||
|
|
|
|||
|
|
@ -47,6 +47,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
|
||||
|
|
@ -83,6 +84,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,
|
||||
|
|
@ -112,6 +114,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)
|
||||
|
|
@ -278,6 +285,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
onBack = router::pop,
|
||||
modifier = modifier,
|
||||
wcContent = { wcRoutingComponent.Content(it) },
|
||||
promoContent = { campaignsComponent.Content(it) },
|
||||
hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) },
|
||||
rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) },
|
||||
scanFailsContent = { scanFailsComponent.Content(it) },
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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<CampaignsDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create(any()) } returns mockk()
|
||||
}
|
||||
|
||||
private val marketsTokenExchangesDeepLinkFactory =
|
||||
mockk<MarketsTokenExchangesDeepLinkHandler.Factory>(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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.features.promobanners.api.deeplink
|
||||
|
||||
interface CampaignsDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(queryParams: Map<String, String>): CampaignsDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<CampaignsBottomSheetConfig> = SlotNavigation()
|
||||
|
||||
init {
|
||||
// campaignsService.campaignFlow
|
||||
// .onEach { campaignId -> resolveStartNavigation(campaignIdConverter.convert(campaignId)) }
|
||||
// .launchIn(modelScope)
|
||||
campaignsService.campaignFlow
|
||||
.onEach { campaignId -> resolveStartNavigation(campaignIdConverter.convert(campaignId)) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue