Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-29 10:51:54 +01:00
parent 92034d6106
commit 625cf611f2
9 changed files with 151 additions and 7 deletions

View file

@ -290,6 +290,17 @@
android:host="tangem.com"
android:path="/pay-app" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="tangem.com"
android:pathPrefix="/news" />
</intent-filter>
</activity>
<!-- Disable android.startup completely. Used for Worker according doc -->

View file

@ -727,6 +727,16 @@ internal class ChildFactory @Inject constructor(
componentFactory = yieldSupplyActiveComponentFactory,
)
}
is AppRoute.NewsDetails -> {
createComponentChild(
context = context,
params = FeedEntryRoute.NewsDetail(
articleId = route.newsId,
preselectedArticlesId = listOf(route.newsId),
),
componentFactory = feedEntryComponentFactory,
)
}
}
}
}

View file

@ -6,6 +6,8 @@ import com.tangem.common.routing.DeepLinkRoute
import com.tangem.common.routing.DeepLinkScheme
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
@ -50,6 +52,8 @@ internal class DeepLinkFactory @Inject constructor(
private val swapDeepLink: SwapDeepLinkHandler.Factory,
private val promoDeepLink: PromoDeeplinkHandler.Factory,
private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory,
private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory,
private val feedFeatureToggle: FeedFeatureToggle,
) {
private val permittedAppRoute = MutableStateFlow(false)
@ -102,7 +106,7 @@ internal class DeepLinkFactory @Inject constructor(
private fun launchDeepLink(deeplinkUri: Uri, coroutineScope: CoroutineScope, isFromOnNewIntent: Boolean) {
when (deeplinkUri.scheme) {
DeepLinkScheme.Https.scheme -> handleHttpDeepLinks(deeplinkUri)
DeepLinkScheme.Https.scheme -> handleHttpDeepLinks(deeplinkUri, coroutineScope)
DeepLinkScheme.Tangem.scheme -> handleTangemDeepLinks(deeplinkUri, coroutineScope, isFromOnNewIntent)
DeepLinkScheme.WalletConnect.scheme -> walletConnectDeepLink.create(deeplinkUri)
else -> {
@ -116,10 +120,18 @@ internal class DeepLinkFactory @Inject constructor(
}
}
private fun handleHttpDeepLinks(deeplinkUri: Uri) {
if (deeplinkUri.host == DeepLinkRoute.PayApp.host && deeplinkUri.path?.startsWith("/pay-app") == true) {
onboardVisaDeepLink.create(deeplinkUri)
return
private fun handleHttpDeepLinks(deeplinkUri: Uri, coroutineScope: CoroutineScope) {
if (deeplinkUri.host == DeepLinkRoute.PayApp.host) {
when {
deeplinkUri.path?.startsWith("/pay-app") == true -> {
onboardVisaDeepLink.create(deeplinkUri)
return
}
deeplinkUri.path?.startsWith("/news") == true && feedFeatureToggle.isFeedEnabled -> {
newsDetailsDeepLink.create(coroutineScope, deeplinkUri)
return
}
}
}
}

View file

@ -4,6 +4,8 @@ import android.net.Uri
import com.tangem.common.routing.AppRoute
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
@ -81,6 +83,12 @@ class DeepLinkFactoryTest {
private val cardSdkProvider = mockk<CardSdkProvider>(relaxed = true) {
every { sdk.uiVisibility() } returns MutableStateFlow(false)
}
private val newsDeeplink = mockk<NewsDetailsDeepLinkHandler.Factory>(relaxed = true) {
every { create(any(), any()) } returns mockk()
}
private val feedFeatureToggle = mockk<FeedFeatureToggle>()
private val mockedUri = mockk<Uri>(relaxed = true)
private val isFromOnNewIntent: Boolean = false
@ -103,6 +111,8 @@ class DeepLinkFactoryTest {
swapDeepLink = swapDeepLinkFactory,
promoDeepLink = promoDeepLinkFactory,
onboardVisaDeepLink = onboardVisaDeepLink,
newsDetailsDeepLink = newsDeeplink,
feedFeatureToggle = feedFeatureToggle,
)
@OptIn(ExperimentalCoroutinesApi::class)

View file

@ -468,4 +468,7 @@ sealed class AppRoute(val path: String) : Route {
val cryptoCurrency: CryptoCurrency,
val apy: String,
) : AppRoute(path = "/yield_supply_active/${userWalletId.stringValue}/${cryptoCurrency.symbol}")
@Serializable
data class NewsDetails(val newsId: Int) : AppRoute(path = "/news_details/$newsId")
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.feed.entry.deeplink
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
interface NewsDetailsDeepLinkHandler {
interface Factory {
fun create(coroutineScope: CoroutineScope, deeplinkUri: Uri): NewsDetailsDeepLinkHandler
}
}

View file

@ -0,0 +1,61 @@
package com.tangem.features.feed.deeplink
import android.net.Uri
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
internal class DefaultNewsDetailsDeepLinkHandler @AssistedInject constructor(
@Assisted private val scope: CoroutineScope,
@Assisted private val deeplinkUri: Uri,
private val appRouter: AppRouter,
) : NewsDetailsDeepLinkHandler {
init {
handleDeepLink()
}
private fun handleDeepLink() {
scope.launch {
val articleId = extractArticleIdFromUri(deeplinkUri)
if (articleId == null) {
Timber.e(
"""
Failed to extract article ID from deep link
|- Received URI: $deeplinkUri
""".trimIndent(),
)
return@launch
}
appRouter.push(
AppRoute.NewsDetails(newsId = articleId),
)
}
}
/**
* Parsing URI with format https://tangem.com/news/{category}/{id}-{slug} to get id
*/
private fun extractArticleIdFromUri(uri: Uri): Int? {
val path = uri.path ?: return null
val pathSegments = path.split("/").filter { it.isNotBlank() }
if (pathSegments.isEmpty() || pathSegments[0] != "news" || pathSegments.size < 2) {
return null
}
val lastSegment = pathSegments.last()
val idPart = lastSegment.substringBefore("-")
return idPart.toIntOrNull()
}
@AssistedFactory
interface Factory : NewsDetailsDeepLinkHandler.Factory {
override fun create(coroutineScope: CoroutineScope, deeplinkUri: Uri): DefaultNewsDetailsDeepLinkHandler
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.feed.deeplink.di
import com.tangem.features.feed.deeplink.DefaultNewsDetailsDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface FeedDeepLinkModule {
@Binds
@Singleton
fun bindNewsDetailsDeepLinkHandlerFactory(
impl: DefaultNewsDetailsDeepLinkHandler.Factory,
): NewsDetailsDeepLinkHandler.Factory
}

View file

@ -27,6 +27,7 @@ import com.tangem.common.ui.news.ArticleHeader
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.pager.PagerIndicator
@ -171,7 +172,10 @@ private fun ArticleDetail(article: ArticleUM, modifier: Modifier = Modifier, onL
items = article.sources,
key = SourceUM::id,
) { source ->
SourceItem(source = source)
SourceItem(
source = source,
modifier = Modifier.fillParentMaxHeight(),
)
}
}
}
@ -259,9 +263,11 @@ private fun SourceItem(source: SourceUM, modifier: Modifier = Modifier) {
color = TangemTheme.colors.text.primary1,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(bottom = 12.dp),
)
}
SpacerHMax()
Text(
text = source.publishedAt.resolveReference(),
style = TangemTheme.typography.caption2,