Updated on 2026-08-14
This commit is contained in:
parent
552da5be18
commit
4addc9601e
15 changed files with 204 additions and 3 deletions
|
|
@ -97,6 +97,7 @@ dependencies {
|
||||||
implementation(projects.domain.manageTokens)
|
implementation(projects.domain.manageTokens)
|
||||||
implementation(projects.domain.onramp)
|
implementation(projects.domain.onramp)
|
||||||
implementation(projects.domain.promo)
|
implementation(projects.domain.promo)
|
||||||
|
implementation(projects.domain.promo.models)
|
||||||
|
|
||||||
implementation(projects.common)
|
implementation(projects.common)
|
||||||
implementation(projects.common.routing)
|
implementation(projects.common.routing)
|
||||||
|
|
|
||||||
|
|
@ -30,4 +30,10 @@ internal object PromoDomainModule {
|
||||||
fun provideShouldShowSwapStoriesUseCase(promoRepository: PromoRepository): ShouldShowSwapStoriesUseCase {
|
fun provideShouldShowSwapStoriesUseCase(promoRepository: PromoRepository): ShouldShowSwapStoriesUseCase {
|
||||||
return ShouldShowSwapStoriesUseCase(promoRepository)
|
return ShouldShowSwapStoriesUseCase(promoRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideGetStoryContentUseCase(promoRepository: PromoRepository): GetStoryContentUseCase {
|
||||||
|
return GetStoryContentUseCase(promoRepository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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<StorySlideResponse>?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class StorySlideResponse(
|
||||||
|
@Json(name = "id") val id: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.api.tangemTech
|
||||||
|
|
||||||
import com.tangem.datasource.api.common.response.ApiResponse
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
import com.tangem.datasource.api.promotion.models.PromotionInfoResponse
|
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.tangemTech.models.*
|
||||||
import com.tangem.datasource.api.utils.ReadTimeout
|
import com.tangem.datasource.api.utils.ReadTimeout
|
||||||
import com.tangem.datasource.local.config.providers.models.ProviderModel
|
import com.tangem.datasource.local.config.providers.models.ProviderModel
|
||||||
|
|
@ -125,6 +126,12 @@ interface TangemTechApi {
|
||||||
@GET("hot_crypto")
|
@GET("hot_crypto")
|
||||||
suspend fun getHotCrypto(@Query("currency") currencyId: String): ApiResponse<HotCryptoResponse>
|
suspend fun getHotCrypto(@Query("currency") currencyId: String): ApiResponse<HotCryptoResponse>
|
||||||
|
|
||||||
|
@GET("stories/{story_id}")
|
||||||
|
suspend fun getStoryById(
|
||||||
|
@Path("story_id") storyId: String,
|
||||||
|
@Query("language") language: String,
|
||||||
|
): ApiResponse<StoryContentResponse>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val marketsQuoteFields = listOf(
|
val marketsQuoteFields = listOf(
|
||||||
"price",
|
"price",
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<StoryContentResponse>,
|
||||||
|
) : PromoStoriesStore {
|
||||||
|
override suspend fun getSyncOrNull(storyId: String): StoryContentResponse? {
|
||||||
|
return dataStore.getSyncOrNull(storyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun store(storyId: String, item: StoryContentResponse) {
|
||||||
|
dataStore.store(storyId, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -53,7 +53,11 @@ inline fun <reified T : StoryConfig> StoriesContainer(
|
||||||
StoriesClickableArea(
|
StoriesClickableArea(
|
||||||
onPress = { isPressed = it },
|
onPress = { isPressed = it },
|
||||||
onPreviousStory = storyState::prevStory,
|
onPreviousStory = storyState::prevStory,
|
||||||
onNextStory = storyState::nextStory,
|
onNextStory = {
|
||||||
|
if (storyState.nextStory()) {
|
||||||
|
config.onClose()
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
currentStoryContent(storyState.currentStory, isPaused)
|
currentStoryContent(storyState.currentStory, isPaused)
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,31 @@
|
||||||
package com.tangem.data.promo
|
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.AppPreferencesStore
|
||||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SWAP_STORIES_KEY
|
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.get
|
||||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||||
import com.tangem.datasource.local.preferences.utils.store
|
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.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.Flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
internal class DefaultPromoRepository(
|
internal class DefaultPromoRepository(
|
||||||
|
private val tangemApi: TangemTechApi,
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
|
private val promoStoriesStore: PromoStoriesStore,
|
||||||
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
) : PromoRepository {
|
) : PromoRepository {
|
||||||
|
|
||||||
|
private val storyContentConverter = StoryContentResponseConverter()
|
||||||
|
|
||||||
override fun isReadyToShowWalletSwapPromo(): Flow<Boolean> {
|
override fun isReadyToShowWalletSwapPromo(): Flow<Boolean> {
|
||||||
return flowOf(false) // Use it on new promo action
|
return flowOf(false) // Use it on new promo action
|
||||||
}
|
}
|
||||||
|
|
@ -29,6 +42,23 @@ internal class DefaultPromoRepository(
|
||||||
// Use it on new promo action
|
// 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<Boolean> {
|
override fun isReadyToShowSwapStories(): Flow<Boolean> {
|
||||||
return appPreferencesStore.get(SHOULD_SHOW_SWAP_STORIES_KEY, true)
|
return appPreferencesStore.get(SHOULD_SHOW_SWAP_STORIES_KEY, true)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<StoryContentResponse, StoryContent> {
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package com.tangem.data.promo.di
|
package com.tangem.data.promo.di
|
||||||
|
|
||||||
import com.tangem.data.promo.DefaultPromoRepository
|
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.preferences.AppPreferencesStore
|
||||||
|
import com.tangem.datasource.local.promo.PromoStoriesStore
|
||||||
import com.tangem.domain.promo.PromoRepository
|
import com.tangem.domain.promo.PromoRepository
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -15,7 +18,17 @@ internal object PromoDataModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providePromoRepository(appPreferencesStore: AppPreferencesStore): PromoRepository {
|
fun providePromoRepository(
|
||||||
return DefaultPromoRepository(appPreferencesStore)
|
tangemTechApi: TangemTechApi,
|
||||||
|
appPreferencesStore: AppPreferencesStore,
|
||||||
|
promoStoriesStore: PromoStoriesStore,
|
||||||
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
|
): PromoRepository {
|
||||||
|
return DefaultPromoRepository(
|
||||||
|
tangemApi = tangemTechApi,
|
||||||
|
appPreferencesStore = appPreferencesStore,
|
||||||
|
promoStoriesStore = promoStoriesStore,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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<StorySlide>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StorySlide(
|
||||||
|
val id: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getImageUrls(): List<String> {
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
|
@ -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<Throwable, StoryContent> = Either.catch {
|
||||||
|
promoRepository.getStoryById(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package com.tangem.domain.promo
|
package com.tangem.domain.promo
|
||||||
|
|
||||||
|
import com.tangem.domain.promo.models.StoryContent
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface PromoRepository {
|
interface PromoRepository {
|
||||||
|
|
||||||
|
// region Promo
|
||||||
fun isReadyToShowWalletSwapPromo(): Flow<Boolean>
|
fun isReadyToShowWalletSwapPromo(): Flow<Boolean>
|
||||||
|
|
||||||
fun isReadyToShowTokenSwapPromo(): Flow<Boolean>
|
fun isReadyToShowTokenSwapPromo(): Flow<Boolean>
|
||||||
|
|
@ -11,10 +13,15 @@ interface PromoRepository {
|
||||||
suspend fun setNeverToShowWalletSwapPromo()
|
suspend fun setNeverToShowWalletSwapPromo()
|
||||||
|
|
||||||
suspend fun setNeverToShowTokenSwapPromo()
|
suspend fun setNeverToShowTokenSwapPromo()
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Stories
|
||||||
|
suspend fun getStoryById(id: String): StoryContent
|
||||||
|
|
||||||
fun isReadyToShowSwapStories(): Flow<Boolean>
|
fun isReadyToShowSwapStories(): Flow<Boolean>
|
||||||
|
|
||||||
suspend fun isReadyToShowSwapStoriesSync(): Boolean
|
suspend fun isReadyToShowSwapStoriesSync(): Boolean
|
||||||
|
|
||||||
suspend fun setNeverToShowSwapStories()
|
suspend fun setNeverToShowSwapStories()
|
||||||
|
// endregion
|
||||||
}
|
}
|
||||||
|
|
@ -39,6 +39,7 @@ dependencies {
|
||||||
implementation(projects.domain.staking)
|
implementation(projects.domain.staking)
|
||||||
implementation(projects.domain.feedback)
|
implementation(projects.domain.feedback)
|
||||||
implementation(projects.domain.promo)
|
implementation(projects.domain.promo)
|
||||||
|
implementation(projects.domain.promo.models)
|
||||||
|
|
||||||
/** Feature modules */
|
/** Feature modules */
|
||||||
implementation(projects.features.swap.domain)
|
implementation(projects.features.swap.domain)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue