diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7500382879..2f2510944d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -97,6 +97,7 @@ dependencies { implementation(projects.domain.manageTokens) implementation(projects.domain.onramp) implementation(projects.domain.promo) + implementation(projects.domain.promo.models) implementation(projects.common) implementation(projects.common.routing) diff --git a/app/src/main/java/com/tangem/tap/di/domain/PromoDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/PromoDomainModule.kt index af52907af8..b741fef075 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/PromoDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/PromoDomainModule.kt @@ -30,4 +30,10 @@ internal object PromoDomainModule { fun provideShouldShowSwapStoriesUseCase(promoRepository: PromoRepository): ShouldShowSwapStoriesUseCase { return ShouldShowSwapStoriesUseCase(promoRepository) } + + @Provides + @Singleton + fun provideGetStoryContentUseCase(promoRepository: PromoRepository): GetStoryContentUseCase { + return GetStoryContentUseCase(promoRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/StoryContentResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/StoryContentResponse.kt new file mode 100644 index 0000000000..b376d2392c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/StoryContentResponse.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.api.promotion.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class StoryContentResponse( + @Json(name = "imageHost") val imageHost: String, + @Json(name = "story") val story: StoryResponse, +) { + + @JsonClass(generateAdapter = true) + data class StoryResponse( + @Json(name = "id") val id: String, + @Json(name = "title") val title: String?, + @Json(name = "slides") val slides: List?, + ) + + @JsonClass(generateAdapter = true) + data class StorySlideResponse( + @Json(name = "id") val id: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 96d2a236d8..4f3b58d971 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.api.tangemTech import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.promotion.models.PromotionInfoResponse +import com.tangem.datasource.api.promotion.models.StoryContentResponse import com.tangem.datasource.api.tangemTech.models.* import com.tangem.datasource.api.utils.ReadTimeout import com.tangem.datasource.local.config.providers.models.ProviderModel @@ -125,6 +126,12 @@ interface TangemTechApi { @GET("hot_crypto") suspend fun getHotCrypto(@Query("currency") currencyId: String): ApiResponse + @GET("stories/{story_id}") + suspend fun getStoryById( + @Path("story_id") storyId: String, + @Query("language") language: String, + ): ApiResponse + companion object { val marketsQuoteFields = listOf( "price", diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt new file mode 100644 index 0000000000..b8af7137ae --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.promo.DefaultPromoStoriesStore +import com.tangem.datasource.local.promo.PromoStoriesStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object PromoStoreModule { + + @Provides + @Singleton + fun providePromoStoriesStore(): PromoStoriesStore { + return DefaultPromoStoriesStore(dataStore = RuntimeDataStore()) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoStoriesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoStoriesStore.kt new file mode 100644 index 0000000000..858f591581 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoStoriesStore.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.local.promo + +import com.tangem.datasource.api.promotion.models.StoryContentResponse +import com.tangem.datasource.local.datastore.core.StringKeyDataStore + +internal class DefaultPromoStoriesStore( + private val dataStore: StringKeyDataStore, +) : PromoStoriesStore { + override suspend fun getSyncOrNull(storyId: String): StoryContentResponse? { + return dataStore.getSyncOrNull(storyId) + } + + override suspend fun store(storyId: String, item: StoryContentResponse) { + dataStore.store(storyId, item) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoStoriesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoStoriesStore.kt new file mode 100644 index 0000000000..73e2fb1d46 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoStoriesStore.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.local.promo + +import com.tangem.datasource.api.promotion.models.StoryContentResponse + +interface PromoStoriesStore { + suspend fun getSyncOrNull(storyId: String): StoryContentResponse? + + suspend fun store(storyId: String, item: StoryContentResponse) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt index 0bcf8ace95..40fb5e4ccf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt @@ -53,7 +53,11 @@ inline fun StoriesContainer( StoriesClickableArea( onPress = { isPressed = it }, onPreviousStory = storyState::prevStory, - onNextStory = storyState::nextStory, + onNextStory = { + if (storyState.nextStory()) { + config.onClose() + } + }, ) currentStoryContent(storyState.currentStory, isPaused) diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index c481880b51..0bf98afda0 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -1,18 +1,31 @@ package com.tangem.data.promo +import com.tangem.data.promo.converters.StoryContentResponseConverter +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SWAP_STORIES_KEY import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store +import com.tangem.datasource.local.promo.PromoStoriesStore import com.tangem.domain.promo.PromoRepository +import com.tangem.domain.promo.models.StoryContent +import com.tangem.utils.SupportedLanguages +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.withContext internal class DefaultPromoRepository( + private val tangemApi: TangemTechApi, private val appPreferencesStore: AppPreferencesStore, + private val promoStoriesStore: PromoStoriesStore, + private val dispatchers: CoroutineDispatcherProvider, ) : PromoRepository { + private val storyContentConverter = StoryContentResponseConverter() + override fun isReadyToShowWalletSwapPromo(): Flow { return flowOf(false) // Use it on new promo action } @@ -29,6 +42,23 @@ internal class DefaultPromoRepository( // Use it on new promo action } + override suspend fun getStoryById(id: String): StoryContent = withContext(dispatchers.io) { + val storedPromo = promoStoriesStore.getSyncOrNull(storyId = id) + // Get last stored promo by id if possible or get from network + val story = if (storedPromo == null) { + val storyContent = tangemApi.getStoryById( + storyId = id, + language = SupportedLanguages.getCurrentSupportedLanguageCode(), + ).getOrThrow() + promoStoriesStore.store(storyId = id, item = storyContent) + storyContent + } else { + storedPromo + } + + storyContentConverter.convert(story) + } + override fun isReadyToShowSwapStories(): Flow { return appPreferencesStore.get(SHOULD_SHOW_SWAP_STORIES_KEY, true) } diff --git a/data/promo/src/main/java/com/tangem/data/promo/converters/StoryContentResponseConverter.kt b/data/promo/src/main/java/com/tangem/data/promo/converters/StoryContentResponseConverter.kt new file mode 100644 index 0000000000..5cf9738597 --- /dev/null +++ b/data/promo/src/main/java/com/tangem/data/promo/converters/StoryContentResponseConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.data.promo.converters + +import com.tangem.datasource.api.promotion.models.StoryContentResponse +import com.tangem.domain.promo.models.StoryContent +import com.tangem.utils.converter.Converter + +internal class StoryContentResponseConverter : Converter { + override fun convert(value: StoryContentResponse): StoryContent { + return StoryContent( + imageHost = value.imageHost, + story = StoryContent.Story( + id = value.story.id, + title = value.story.title, + slides = value.story.slides?.map { slide -> + StoryContent.StorySlide( + id = slide.id, + ) + }.orEmpty(), + ), + ) + } +} \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt index 40499944ec..a564ef0509 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt @@ -1,8 +1,11 @@ package com.tangem.data.promo.di import com.tangem.data.promo.DefaultPromoRepository +import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.promo.PromoStoriesStore import com.tangem.domain.promo.PromoRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,7 +18,17 @@ internal object PromoDataModule { @Provides @Singleton - fun providePromoRepository(appPreferencesStore: AppPreferencesStore): PromoRepository { - return DefaultPromoRepository(appPreferencesStore) + fun providePromoRepository( + tangemTechApi: TangemTechApi, + appPreferencesStore: AppPreferencesStore, + promoStoriesStore: PromoStoriesStore, + dispatchers: CoroutineDispatcherProvider, + ): PromoRepository { + return DefaultPromoRepository( + tangemApi = tangemTechApi, + appPreferencesStore = appPreferencesStore, + promoStoriesStore = promoStoriesStore, + dispatchers = dispatchers, + ) } } \ No newline at end of file diff --git a/domain/promo/models/src/main/java/com/tangem/domain/promo/models/StoryContent.kt b/domain/promo/models/src/main/java/com/tangem/domain/promo/models/StoryContent.kt new file mode 100644 index 0000000000..c25f570eea --- /dev/null +++ b/domain/promo/models/src/main/java/com/tangem/domain/promo/models/StoryContent.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.promo.models + +data class StoryContent( + val imageHost: String, + val story: Story, +) { + data class Story( + val id: String, + val title: String?, + val slides: List, + ) + + data class StorySlide( + val id: String, + ) + + fun getImageUrls(): List { + return story.slides.map { slide -> imageHost + slide.id + WEBP_EXTENSION } + } + + private companion object { + const val WEBP_EXTENSION = ".webp" + } +} + +enum class StoryContentIds(val id: String) { + STORY_FIRST_TIME_SWAP("first-time-swap"), +} \ No newline at end of file diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/GetStoryContentUseCase.kt b/domain/promo/src/main/java/com/tangem/domain/promo/GetStoryContentUseCase.kt new file mode 100644 index 0000000000..6866cd558b --- /dev/null +++ b/domain/promo/src/main/java/com/tangem/domain/promo/GetStoryContentUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.promo + +import arrow.core.Either +import com.tangem.domain.promo.models.StoryContent + +class GetStoryContentUseCase( + private val promoRepository: PromoRepository, +) { + + suspend operator fun invoke(id: String): Either = Either.catch { + promoRepository.getStoryById(id) + } +} \ No newline at end of file diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt index cc66ada854..012806c3f9 100644 --- a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt @@ -1,9 +1,11 @@ package com.tangem.domain.promo +import com.tangem.domain.promo.models.StoryContent import kotlinx.coroutines.flow.Flow interface PromoRepository { + // region Promo fun isReadyToShowWalletSwapPromo(): Flow fun isReadyToShowTokenSwapPromo(): Flow @@ -11,10 +13,15 @@ interface PromoRepository { suspend fun setNeverToShowWalletSwapPromo() suspend fun setNeverToShowTokenSwapPromo() + // endregion + + // region Stories + suspend fun getStoryById(id: String): StoryContent fun isReadyToShowSwapStories(): Flow suspend fun isReadyToShowSwapStoriesSync(): Boolean suspend fun setNeverToShowSwapStories() + // endregion } \ No newline at end of file diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 3049182a5f..a07e4897ad 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(projects.domain.staking) implementation(projects.domain.feedback) implementation(projects.domain.promo) + implementation(projects.domain.promo.models) /** Feature modules */ implementation(projects.features.swap.domain)