diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9423329489..0b2d7e6df6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -248,6 +248,8 @@ dependencies { implementation(projects.features.walletSettings.impl) implementation(projects.features.markets.api) implementation(projects.features.markets.impl) + implementation(projects.features.feed.api) + implementation(projects.features.feed.impl) implementation(projects.features.onramp.api) implementation(projects.features.onramp.impl) implementation(projects.features.onboardingV2.api) diff --git a/features/feed/api/.gitignore b/features/feed/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/feed/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/feed/api/build.gradle.kts b/features/feed/api/build.gradle.kts new file mode 100644 index 0000000000..73aa93625c --- /dev/null +++ b/features/feed/api/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + id("kotlin-parcelize") + id("configuration") +} + +android { + namespace = "com.tangem.features.feed.api" +} + +dependencies { + implementation(deps.compose.foundation) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Project - Domain */ + implementation(projects.domain.core) + implementation(projects.domain.tokens.models) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.markets.models) +} \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/BottomSheetState.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/BottomSheetState.kt new file mode 100644 index 0000000000..9b58eb7690 --- /dev/null +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/BottomSheetState.kt @@ -0,0 +1,6 @@ +package com.tangem.features.feed.entry + +enum class BottomSheetState { + EXPANDED, + COLLAPSED, +} \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt new file mode 100644 index 0000000000..ad7aab9506 --- /dev/null +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt @@ -0,0 +1,24 @@ +package com.tangem.features.feed.entry.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.features.feed.entry.BottomSheetState + +@Stable +interface FeedEntryComponent { + + @Composable + fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) + + interface Factory { + fun create(context: AppComponentContext): FeedEntryComponent + } +} \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt new file mode 100644 index 0000000000..c04c556450 --- /dev/null +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt @@ -0,0 +1,5 @@ +package com.tangem.features.feed.entry.featuretoggle + +interface FeedFeatureToggle { + val isFeedEnabled: Boolean +} \ No newline at end of file diff --git a/features/feed/impl/.gitignore b/features/feed/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/feed/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts new file mode 100644 index 0000000000..2a54428d6f --- /dev/null +++ b/features/feed/impl/build.gradle.kts @@ -0,0 +1,96 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.feed.impl" +} + +dependencies { + /* Project - API */ + api(projects.features.feed.api) + api(projects.features.onramp.api) + api(projects.features.sendV2.api) + api(projects.features.tokenRecieve.api) + api(projects.features.wallet.api) + api(projects.features.account.api) + + /* Data */ + implementation(projects.data.common) + + /* Domain */ + implementation(projects.domain.account) + implementation(projects.domain.account.status) + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.card) + implementation(projects.domain.demo) + implementation(projects.domain.feedback) + implementation(projects.domain.feedback.models) + implementation(projects.domain.manageTokens) + implementation(projects.domain.markets) + implementation(projects.domain.onramp.models) + implementation(projects.domain.staking.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + implementation(projects.domain.settings) + implementation(projects.domain.notifications.models) + implementation(projects.domain.transaction) + + // FIXME [REDACTED_TASK_KEY] + // Remove the "Buy" and "Sell" actions from the redux middleware. + // Instead, create some kind of interface for such cases. + /* Redux -_- */ + implementation(projects.domain.legacy) + implementation(deps.reKotlin) + + /* Compose */ + implementation(deps.compose.coil) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.ui.utils) + implementation(deps.lifecycle.compose) + implementation(deps.androidx.activity.compose) + implementation(deps.markdown.composeview) + + /* DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /* Other */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) + implementation(deps.decompose.ext.compose) + + /* Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.configToggles) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.navigation) + + /* Common */ + implementation(projects.common.ui) + implementation(projects.common.uiCharts) + implementation(projects.common.routing) + + /* Libs */ + implementation(projects.libs.crypto) + implementation(projects.libs.blockchainSdk) + + /** Tangem libraries */ + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt new file mode 100644 index 0000000000..cdf3a46598 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt @@ -0,0 +1,91 @@ +package com.tangem.features.feed.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.popWhile +import com.arkivanov.decompose.value.Value +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent +import com.tangem.features.feed.entry.BottomSheetState +import com.tangem.features.feed.entry.components.FeedEntryComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultFeedEntryComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + private val feedEntryChildFactory: FeedEntryChildFactory, +) : FeedEntryComponent, AppComponentContext by context { + + private val stackNavigation = StackNavigation() + + private val innerRouter = InnerRouter( + stackNavigation = stackNavigation, + popCallback = { onChildBack() }, + ) + + private val stack: Value> = childStack( + key = "main", + source = stackNavigation, + serializer = FeedEntryChildFactory.Child.serializer(), + initialConfiguration = FeedEntryChildFactory.Child.Feed, + handleBackButton = false, + childFactory = { configuration, factoryContext -> + feedEntryChildFactory.createChild( + child = configuration, + appComponentContext = childByContext( + componentContext = factoryContext, + router = innerRouter, + ), + onTokenClick = ::marketsListTokenSelected, + ) + }, + ) + + @Composable + override fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) { + bottomSheetState // TODO will be continued in next tasks. + } + + private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) { + innerRouter.push( + route = FeedEntryChildFactory.Child.TokenDetails( + params = DefaultMarketsTokenDetailsComponent.Params( + token = token, + appCurrency = appCurrency, + shouldShowPortfolio = true, + analyticsParams = DefaultMarketsTokenDetailsComponent.AnalyticsParams( + blockchain = null, + source = "Market", + ), + ), + ), + ) + } + + private fun onChildBack() { + if (stack.value.active.configuration !is FeedEntryChildFactory.Child.Feed) { + stackNavigation.popWhile { it != FeedEntryChildFactory.Child.Feed } + } + } + + @AssistedFactory + interface Factory : FeedEntryComponent.Factory { + override fun create(context: AppComponentContext): DefaultFeedEntryComponent + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt new file mode 100644 index 0000000000..c33781008b --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -0,0 +1,77 @@ +package com.tangem.features.feed.components + +import androidx.compose.runtime.Immutable +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.features.feed.components.feed.DefaultFeedComponent +import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent +import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent +import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent +import com.tangem.features.feed.components.news.list.DefaultNewsListComponent +import kotlinx.serialization.Serializable + +internal class FeedEntryChildFactory { + + @Serializable + @Immutable + sealed interface Child : Route { + + @Serializable + @Immutable + data object Feed : Child + + @Serializable + @Immutable + data object TokenList : Child + + @Serializable + @Immutable + data class TokenDetails(val params: DefaultMarketsTokenDetailsComponent.Params) : Child + + @Serializable + @Immutable + data object NewsList : Child + + @Serializable + @Immutable + data object NewsDetails : Child + } + + fun createChild( + child: Child, + appComponentContext: AppComponentContext, + onTokenClick: (TokenMarketParams, AppCurrency) -> Unit, + ): Any { + return when (child) { + is Child.TokenDetails -> { + DefaultMarketsTokenDetailsComponent( + appComponentContext = appComponentContext, + params = child.params, + ) + } + is Child.TokenList -> { + DefaultMarketsTokenListComponent( + appComponentContext = appComponentContext, + onTokenClick = onTokenClick, + ) + } + Child.NewsDetails -> { + DefaultNewsDetailsComponent( + appComponentContext = appComponentContext, + ) + } + Child.NewsList -> { + DefaultNewsListComponent( + appComponentContext = appComponentContext, + ) + } + Child.Feed -> { + DefaultFeedComponent( + appComponentContext = appComponentContext, + ) + } + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt new file mode 100644 index 0000000000..a6bd119f49 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -0,0 +1,23 @@ +package com.tangem.features.feed.components.feed + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableModularContentComponent + +internal class DefaultFeedComponent( + appComponentContext: AppComponentContext, +) : ComposableModularContentComponent, AppComponentContext by appComponentContext { + + @Composable + override fun Title() { + } + + @Composable + override fun Content(modifier: Modifier) { + } + + @Composable + override fun Footer() { + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt new file mode 100644 index 0000000000..f6933c66cd --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt @@ -0,0 +1,41 @@ +package com.tangem.features.feed.components.market.details + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableModularContentComponent +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketParams +import kotlinx.serialization.Serializable + +internal class DefaultMarketsTokenDetailsComponent( + appComponentContext: AppComponentContext, + val params: Params, +) : ComposableModularContentComponent, AppComponentContext by appComponentContext { + + @Composable + override fun Title() { + } + + @Composable + override fun Content(modifier: Modifier) { + } + + @Composable + override fun Footer() { + } + + @Serializable + data class Params( + val token: TokenMarketParams, + val appCurrency: AppCurrency, + val shouldShowPortfolio: Boolean, + val analyticsParams: AnalyticsParams?, + ) + + @Serializable + data class AnalyticsParams( + val blockchain: String?, + val source: String, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt new file mode 100644 index 0000000000..ca5d351c48 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt @@ -0,0 +1,27 @@ +package com.tangem.features.feed.components.market.list + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableModularContentComponent +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketParams + +@Suppress("UnusedPrivateProperty") // TODO will be remove in next PR +internal class DefaultMarketsTokenListComponent( + appComponentContext: AppComponentContext, + private val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit)? = null, +) : ComposableModularContentComponent, AppComponentContext by appComponentContext { + + @Composable + override fun Title() { + } + + @Composable + override fun Content(modifier: Modifier) { + } + + @Composable + override fun Footer() { + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt new file mode 100644 index 0000000000..bad826640c --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt @@ -0,0 +1,23 @@ +package com.tangem.features.feed.components.news.details + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableModularContentComponent + +internal class DefaultNewsDetailsComponent( + appComponentContext: AppComponentContext, +) : ComposableModularContentComponent, AppComponentContext by appComponentContext { + + @Composable + override fun Title() { + } + + @Composable + override fun Content(modifier: Modifier) { + } + + @Composable + override fun Footer() { + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/list/DefaultNewsListComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/list/DefaultNewsListComponent.kt new file mode 100644 index 0000000000..bdde19a5c8 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/list/DefaultNewsListComponent.kt @@ -0,0 +1,23 @@ +package com.tangem.features.feed.components.news.list + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableModularContentComponent + +internal class DefaultNewsListComponent( + appComponentContext: AppComponentContext, +) : ComposableModularContentComponent, AppComponentContext by appComponentContext { + + @Composable + override fun Title() { + } + + @Composable + override fun Content(modifier: Modifier) { + } + + @Composable + override fun Footer() { + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/FeedFeatureToggleModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/FeedFeatureToggleModule.kt new file mode 100644 index 0000000000..7e9bee8655 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/FeedFeatureToggleModule.kt @@ -0,0 +1,23 @@ +package com.tangem.features.feed.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle +import com.tangem.features.feed.featuretoggle.DefaultFeedFeatureToggle +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object FeedFeatureToggleModule { + + @Provides + @Singleton + fun provideFeedFeatureToggle(featureTogglesManager: FeatureTogglesManager): FeedFeatureToggle { + return DefaultFeedFeatureToggle( + featureTogglesManager = featureTogglesManager, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt new file mode 100644 index 0000000000..c15096b261 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt @@ -0,0 +1,12 @@ +package com.tangem.features.feed.featuretoggle + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle + +internal class DefaultFeedFeatureToggle( + private val featureTogglesManager: FeatureTogglesManager, +) : FeedFeatureToggle { + + override val isFeedEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("FEED_ENABLED") +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 4e3af1e28f..5b286caeaf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -294,6 +294,9 @@ include(":features:token-recieve:impl") include(":features:yield-supply:api") include(":features:yield-supply:impl") + +include(":features:feed:api") +include(":features:feed:impl") // endregion Feature modules // region Domain modules