diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ab376d6124..bb289cd06c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -290,6 +290,17 @@ android:host="tangem.com" android:path="/pay-app" /> + + + + + + + + diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 820b018fc2..83d4c9f04b 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -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, + ) + } } } } \ No newline at end of file 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 097b57b518..27c065e869 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 @@ -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 + } + } } } 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 ea548398d0..19782fca91 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 @@ -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(relaxed = true) { every { sdk.uiVisibility() } returns MutableStateFlow(false) } + + private val newsDeeplink = mockk(relaxed = true) { + every { create(any(), any()) } returns mockk() + } + private val feedFeatureToggle = mockk() + private val mockedUri = mockk(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) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index dbb07560d9..ae5ccdd4d1 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -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") } \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/NewsDetailsDeepLinkHandler.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/NewsDetailsDeepLinkHandler.kt new file mode 100644 index 0000000000..f6ce381120 --- /dev/null +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/NewsDetailsDeepLinkHandler.kt @@ -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 + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDetailsDeepLinkHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDetailsDeepLinkHandler.kt new file mode 100644 index 0000000000..8409d68360 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDetailsDeepLinkHandler.kt @@ -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 + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt new file mode 100644 index 0000000000..d00b30cc79 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt @@ -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 +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt index 61ef4bc6c9..adb45733c1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt @@ -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,