Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-18 08:47:52 +01:00
parent 9105ffbd75
commit dcb8b474cf
20 changed files with 550 additions and 8 deletions

View file

@ -54,5 +54,9 @@
{
"name": "ACCOUNTS_FEATURE_ENABLED",
"version": "undefined"
},
{
"name": "FEED_ENABLED",
"version": "undefined"
}
]

View file

@ -29,6 +29,7 @@ sealed class ApiConfig {
BlockAid,
YieldSupply,
MoonPay,
News,
}
private fun initializeId(): ID {
@ -41,6 +42,7 @@ sealed class ApiConfig {
is BlockAid -> ID.BlockAid
is YieldSupply -> ID.YieldSupply
is MoonPay -> ID.MoonPay
is News -> ID.News
}
}

View file

@ -0,0 +1,62 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.Provider
/**
* News [ApiConfig]
[REDACTED_AUTHOR]
*/
internal class News(
private val authProvider: AuthProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createProdEnvironment(),
createDevEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
DEBUG_BUILD_TYPE,
-> ApiEnvironment.DEV
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = PROD_BASE_URL,
headers = createHeaders(ApiEnvironment.PROD),
)
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = DEV_BASE_URL,
headers = createHeaders(ApiEnvironment.DEV),
)
private fun createHeaders(environment: ApiEnvironment) = buildMap {
putAll(
RequestHeader.TangemApiKeyHeader(
authProvider = authProvider,
apiEnvironment = Provider { environment },
).values,
)
}
private companion object {
private const val PROD_BASE_URL = "https://api.tangem.org/"
private const val DEV_BASE_URL = "[REDACTED_ENV_URL]"
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.datasource.api.news
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.news.models.response.NewsCategoriesResponse
import com.tangem.datasource.api.news.models.response.NewsDetailsResponse
import com.tangem.datasource.api.news.models.response.NewsListResponse
import com.tangem.datasource.api.news.models.response.NewsTrendingResponse
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface NewsApi {
@GET(NEWS_PATH)
suspend fun getNews(
@Query("page") page: Int? = null,
@Query("limit") limit: Int? = null,
@Query("lang") language: String? = null,
@Query("asOf") snapshot: String? = null,
@Query("tokenIds") tokenIds: List<String>? = null,
@Query("categoryIds") categoryIds: List<Int>? = null,
): ApiResponse<NewsListResponse>
@GET("$NEWS_PATH/{newsId}")
suspend fun getNewsDetails(
@Path("newsId") newsId: Int,
@Query("lang") language: String? = null,
): ApiResponse<NewsDetailsResponse>
@GET("$NEWS_PATH/trending")
suspend fun getTrendingNews(
@Query("limit") limit: Int? = null,
@Query("lang") language: String? = null,
): ApiResponse<NewsTrendingResponse>
@GET("$NEWS_PATH/categories")
suspend fun getCategories(): ApiResponse<NewsCategoriesResponse>
private companion object {
private const val NEWS_PATH = "api/v1/news"
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.datasource.api.news.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NewsArticleDto(
@Json(name = "id") val id: Int,
@Json(name = "createdAt") val createdAt: String,
@Json(name = "score") val score: Double,
@Json(name = "language") val language: String,
@Json(name = "isTrending") val isTrending: Boolean,
@Json(name = "categories") val categories: List<NewsCategoryDto>,
@Json(name = "relatedTokens") val relatedTokens: List<NewsRelatedTokenDto>,
@Json(name = "title") val title: String,
@Json(name = "newsUrl") val newsUrl: String,
)
@JsonClass(generateAdapter = true)
data class NewsCategoryDto(
@Json(name = "id") val id: Int,
@Json(name = "name") val name: String,
)
@JsonClass(generateAdapter = true)
data class NewsRelatedTokenDto(
@Json(name = "id") val id: String,
@Json(name = "symbol") val symbol: String,
@Json(name = "name") val name: String,
)
@JsonClass(generateAdapter = true)
data class NewsOriginalArticleDto(
@Json(name = "id") val id: Int,
@Json(name = "title") val title: String,
@Json(name = "sourceName") val sourceName: String,
@Json(name = "language") val language: String,
@Json(name = "publishedAt") val publishedAt: String,
@Json(name = "url") val url: String,
@Json(name = "imageUrl") val imageUrl: String? = null,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.news.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NewsCategoriesResponse(
@Json(name = "items") val items: List<NewsCategoryDto>,
)

View file

@ -0,0 +1,20 @@
package com.tangem.datasource.api.news.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NewsDetailsResponse(
@Json(name = "id") val id: Int,
@Json(name = "createdAt") val createdAt: String,
@Json(name = "score") val score: Double,
@Json(name = "language") val language: String,
@Json(name = "isTrending") val isTrending: Boolean,
@Json(name = "categories") val categories: List<NewsCategoryDto>,
@Json(name = "relatedTokens") val relatedTokens: List<NewsRelatedTokenDto>,
@Json(name = "title") val title: String,
@Json(name = "newsUrl") val newsUrl: String,
@Json(name = "shortContent") val shortContent: String,
@Json(name = "content") val content: String,
@Json(name = "originalArticles") val originalArticles: List<NewsOriginalArticleDto>,
)

View file

@ -0,0 +1,19 @@
package com.tangem.datasource.api.news.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NewsListResponse(
@Json(name = "meta") val meta: NewsListMetaDto,
@Json(name = "items") val items: List<NewsArticleDto>,
)
@JsonClass(generateAdapter = true)
data class NewsListMetaDto(
@Json(name = "page") val page: Int,
@Json(name = "limit") val limit: Int,
@Json(name = "total") val total: Long,
@Json(name = "hasNext") val hasNext: Boolean,
@Json(name = "asOf") val asOf: String,
)

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.news.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NewsTrendingResponse(
@Json(name = "meta") val meta: NewsTrendingMetaDto,
@Json(name = "items") val items: List<NewsArticleDto>,
)
@JsonClass(generateAdapter = true)
data class NewsTrendingMetaDto(
@Json(name = "limit") val limit: Int,
)

View file

@ -58,6 +58,10 @@ internal object ApiConfigsModule {
appInfoProvider = appInfoProvider,
)
@Provides
@IntoSet
fun provideNewsConfig(authProvider: AuthProvider): ApiConfig = News(authProvider = authProvider)
@Provides
@IntoSet
fun provideYieldSupplyConfig(

View file

@ -13,6 +13,7 @@ import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.api.news.NewsApi
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
import com.tangem.datasource.api.pay.TangemPayApi
@ -151,4 +152,13 @@ internal object NetworkModule {
applyTimeoutAnnotations = false,
)
}
@Provides
@Singleton
fun provideNewsApi(retrofitApiBuilder: RetrofitApiBuilder): NewsApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.News,
applyTimeoutAnnotations = false,
)
}
}

View file

@ -73,6 +73,7 @@ class ApiConfigTest {
ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk())
ApiConfig.ID.MoonPay -> MoonPay()
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk())
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
}
}
}

View file

@ -114,6 +114,7 @@ internal class ProdApiConfigsManagerTest {
ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage)
ApiConfig.ID.MoonPay -> MoonPay()
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider)
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
}
}
}
@ -128,6 +129,7 @@ internal class ProdApiConfigsManagerTest {
ApiConfig.ID.BlockAid -> createBlockAidSdkModel()
ApiConfig.ID.MoonPay -> createMoonPayModel()
ApiConfig.ID.P2PEthPool -> createP2PModel()
ApiConfig.ID.News -> createNewsModel()
}
}
@ -295,6 +297,29 @@ internal class ProdApiConfigsManagerTest {
)
}
private fun createNewsModel(): TestModel {
val (environment, baseUrl) = when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
DEBUG_BUILD_TYPE,
-> ApiEnvironment.DEV to "[REDACTED_ENV_URL]"
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD to "https://tangem.com/"
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
return TestModel(
id = ApiConfig.ID.News,
expected = ApiEnvironmentConfig(
environment = environment,
baseUrl = baseUrl,
headers = mapOf(
"api-key" to ProviderSuspend { TANGEM_API_KEY },
),
),
)
}
private fun String.checkHeaderValueOrEmpty(): String {
for (i in this.indices) {
val c = this[i]