Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-05 11:55:12 +05:00
parent 552da5be18
commit 4addc9601e
15 changed files with 204 additions and 3 deletions

View file

@ -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)

View file

@ -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)
}
} }

View file

@ -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,
)
}

View file

@ -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",

View file

@ -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())
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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)
} }

View file

@ -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(),
),
)
}
}

View file

@ -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,
)
} }
} }

View file

@ -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"),
}

View file

@ -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)
}
}

View file

@ -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
} }

View file

@ -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)