Updated on 2026-08-14
This commit is contained in:
parent
53e29be3c2
commit
380e5c19e0
16 changed files with 326 additions and 0 deletions
|
|
@ -177,6 +177,8 @@ dependencies {
|
|||
implementation(projects.features.onramp.impl)
|
||||
implementation(projects.features.onboardingV2.api)
|
||||
implementation(projects.features.onboardingV2.impl)
|
||||
implementation(projects.features.stories.api)
|
||||
implementation(projects.features.stories.impl)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.core.ktx)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tangem.common.routing.AppRoute
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.feature.referral.ReferralFragment
|
||||
import com.tangem.feature.stories.api.StoriesComponent
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
|
||||
import com.tangem.features.details.component.DetailsComponent
|
||||
|
|
@ -56,6 +57,7 @@ internal class ChildFactory @Inject constructor(
|
|||
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
|
||||
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
|
||||
private val welcomeComponentFactory: WelcomeComponent.Factory,
|
||||
private val storiesComponentFactory: StoriesComponent.Factory,
|
||||
private val sendRouter: SendRouter,
|
||||
private val tokenDetailsRouter: TokenDetailsRouter,
|
||||
private val walletRouter: WalletRouter,
|
||||
|
|
@ -199,6 +201,16 @@ internal class ChildFactory @Inject constructor(
|
|||
componentFactory = onboardingEntryComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.Stories -> {
|
||||
createComponentChild(
|
||||
contextProvider = contextProvider(route, contextFactory),
|
||||
params = StoriesComponent.Params(
|
||||
storyId = route.storyId,
|
||||
nextScreen = route.nextScreen,
|
||||
),
|
||||
componentFactory = storiesComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.AccessCodeRecovery,
|
||||
is AppRoute.AppCurrencySelector,
|
||||
is AppRoute.SaveWallet,
|
||||
|
|
@ -407,6 +419,16 @@ internal class ChildFactory @Inject constructor(
|
|||
componentFactory = onboardingEntryComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.Stories -> {
|
||||
route.asComponentChild(
|
||||
contextProvider = contextProvider(route, contextFactory),
|
||||
params = StoriesComponent.Params(
|
||||
storyId = route.storyId,
|
||||
nextScreen = route.nextScreen,
|
||||
),
|
||||
componentFactory = storiesComponentFactory,
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -341,4 +341,12 @@ sealed class AppRoute(val path: String) : Route {
|
|||
AddBackup, // continue backup process for existing wallet 1
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Stories(
|
||||
val storyId: String,
|
||||
val nextScreen: AppRoute,
|
||||
) : AppRoute(path = "/stories$storyId"), RouteBundleParams {
|
||||
override fun getBundle(): Bundle = bundle(serializer())
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ dependencies {
|
|||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.promo.models)
|
||||
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.tangem.blockchain) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.common.ui.swapStoriesScreen
|
||||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.promo.models.StoryContent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
object SwapStoriesFactory {
|
||||
|
||||
// WARNING! Be careful with indices. Temporary solution.
|
||||
// Use all data from v1/stories api (image url, title, subtitle)
|
||||
@Suppress("MagicNumber")
|
||||
fun createStoriesState(swapStory: StoryContent, onStoriesClose: () -> Unit): SwapStoriesUM {
|
||||
val storyOrderedImageUrls = swapStory.getImageUrls()
|
||||
if (storyOrderedImageUrls.size != 5) return SwapStoriesUM.Empty
|
||||
|
||||
return SwapStoriesUM.Content(
|
||||
stories = persistentListOf(
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[0],
|
||||
title = resourceReference(R.string.swap_story_first_title),
|
||||
subtitle = resourceReference(R.string.swap_story_first_subtitle),
|
||||
),
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[1],
|
||||
title = resourceReference(R.string.swap_story_second_title),
|
||||
subtitle = resourceReference(R.string.swap_story_second_subtitle),
|
||||
),
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[2],
|
||||
title = resourceReference(R.string.swap_story_third_title),
|
||||
subtitle = resourceReference(R.string.swap_story_third_subtitle),
|
||||
),
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[3],
|
||||
title = resourceReference(R.string.swap_story_forth_title),
|
||||
subtitle = resourceReference(R.string.swap_story_forth_subtitle),
|
||||
),
|
||||
SwapStoriesUM.Content.Config(
|
||||
imageUrl = storyOrderedImageUrls[4],
|
||||
title = resourceReference(R.string.swap_story_fifth_title),
|
||||
subtitle = resourceReference(R.string.swap_story_fifth_subtitle),
|
||||
),
|
||||
),
|
||||
onClose = onStoriesClose,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.common.ui.swapStoriesScreen
|
||||
|
||||
import com.tangem.core.ui.components.stories.model.StoriesContentConfig
|
||||
import com.tangem.core.ui.components.stories.model.StoryConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
sealed class SwapStoriesUM {
|
||||
|
||||
data object Empty : SwapStoriesUM()
|
||||
|
||||
data class Content(
|
||||
override val stories: ImmutableList<Config>,
|
||||
override val onClose: () -> Unit,
|
||||
) : SwapStoriesUM(), StoriesContentConfig<Content.Config> {
|
||||
override val isRestartable: Boolean = false
|
||||
|
||||
data class Config(
|
||||
val imageUrl: String,
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
) : StoryConfig {
|
||||
override val duration: Int = 2000
|
||||
}
|
||||
}
|
||||
}
|
||||
1
features/stories/api/.gitignore
vendored
Normal file
1
features/stories/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
20
features/stories/api/build.gradle.kts
Normal file
20
features/stories/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("kotlin-parcelize")
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.stories.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
implementation(projects.common.routing)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.feature.stories.api
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface StoriesComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val storyId: String,
|
||||
val nextScreen: AppRoute,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, StoriesComponent>
|
||||
}
|
||||
1
features/stories/impl/.gitignore
vendored
Normal file
1
features/stories/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
43
features/stories/impl/build.gradle.kts
Normal file
43
features/stories/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.stories.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Feature modules */
|
||||
implementation(projects.features.stories.api)
|
||||
implementation(projects.features.swap.api)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.promo)
|
||||
implementation(projects.domain.promo.models)
|
||||
|
||||
/** Project - Common */
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Project - Core */
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
implementation(deps.lifecycle.compose)
|
||||
|
||||
/** Others */
|
||||
implementation(deps.timber)
|
||||
implementation(deps.arrow.core)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.feature.stories.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesScreen
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.feature.stories.api.StoriesComponent
|
||||
import com.tangem.feature.stories.impl.model.StoriesModel
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
@Stable
|
||||
internal class DefaultStoriesComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted params: StoriesComponent.Params,
|
||||
) : StoriesComponent, AppComponentContext by context {
|
||||
|
||||
private val model: StoriesModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state = model.state.collectAsStateWithLifecycle()
|
||||
SwapStoriesScreen(state.value)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : StoriesComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: StoriesComponent.Params): DefaultStoriesComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.feature.stories.impl.di
|
||||
|
||||
import com.tangem.feature.stories.api.StoriesComponent
|
||||
import com.tangem.feature.stories.impl.DefaultStoriesComponent
|
||||
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 StoriesComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindSwapStoriesComponentFactory(factory: DefaultStoriesComponent.Factory): StoriesComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.feature.stories.impl.di
|
||||
|
||||
import com.tangem.core.decompose.di.DecomposeComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.feature.stories.impl.model.StoriesModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(DecomposeComponent::class)
|
||||
internal interface StoriesModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(StoriesModel::class)
|
||||
fun bindStoriesModel(model: StoriesModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.feature.stories.impl.model
|
||||
|
||||
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesFactory
|
||||
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesUM
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.domain.promo.GetStoryContentUseCase
|
||||
import com.tangem.domain.promo.ShouldShowSwapStoriesUseCase
|
||||
import com.tangem.domain.promo.models.StoryContentIds
|
||||
import com.tangem.feature.stories.api.StoriesComponent
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class StoriesModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val router: Router,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getStoryContentUseCase: GetStoryContentUseCase,
|
||||
private val shouldShowSwapStoriesUseCase: ShouldShowSwapStoriesUseCase,
|
||||
) : Model() {
|
||||
val state: StateFlow<SwapStoriesUM> get() = _state
|
||||
|
||||
private val params = paramsContainer.require<StoriesComponent.Params>()
|
||||
private val _state: MutableStateFlow<SwapStoriesUM> = MutableStateFlow(
|
||||
value = SwapStoriesUM.Empty,
|
||||
)
|
||||
|
||||
init {
|
||||
initStories()
|
||||
}
|
||||
|
||||
private fun openScreen() {
|
||||
modelScope.launch {
|
||||
shouldShowSwapStoriesUseCase.neverToShow()
|
||||
router.pop()
|
||||
router.push(params.nextScreen)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initStories() {
|
||||
modelScope.launch {
|
||||
getStoryContentUseCase(params.storyId).fold(
|
||||
ifLeft = {
|
||||
Timber.e("Unable to load stories for ${StoryContentIds.STORY_FIRST_TIME_SWAP.id}")
|
||||
openScreen() // Fallback to target screen
|
||||
},
|
||||
ifRight = { swapStory ->
|
||||
_state.update {
|
||||
SwapStoriesFactory.createStoriesState(
|
||||
swapStory = swapStory,
|
||||
onStoriesClose = ::openScreen,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -205,6 +205,9 @@ include(":features:markets:impl")
|
|||
|
||||
include(":features:onramp:api")
|
||||
include(":features:onramp:impl")
|
||||
|
||||
include(":features:stories:api")
|
||||
include(":features:stories:impl")
|
||||
// endregion Feature modules
|
||||
|
||||
// region Domain modules
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue