Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-14 12:15:55 +03:00
parent 38f5c42a94
commit c6d333dbeb
14 changed files with 149 additions and 59 deletions

View file

@ -2,21 +2,23 @@ package com.tangem.datasource.api.common
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.utils.Provider
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import okio.IOException
/**
* Switch base url [Interceptor]
* Switch api environment [Interceptor]
*
* @property id api config id [ApiConfig.ID]
* @property apiConfigsManager api configs manager
*
[REDACTED_AUTHOR]
*/
internal class SwitchBaseUrlInterceptor(
internal class SwitchEnvironmentInterceptor(
private val id: ApiConfig.ID,
private val apiConfigsManager: ApiConfigsManager,
) : Interceptor {
@ -26,18 +28,27 @@ internal class SwitchBaseUrlInterceptor(
var request = chain.request()
val builder = request.newBuilder()
val environmentConfig = apiConfigsManager.getEnvironmentConfig(id)
request = builder
.url(url = request.url.adjustBaseUrl())
.url(url = request.url.adjustBaseUrl(environmentConfig.baseUrl))
.addHeaders(headers = environmentConfig.headers)
.build()
return chain.proceed(request)
}
private fun HttpUrl.adjustBaseUrl(): HttpUrl {
val host = apiConfigsManager.getBaseUrl(id).toHttpUrl().host
private fun HttpUrl.adjustBaseUrl(url: String): HttpUrl {
return this.newBuilder()
.host(host)
.host(host = url.toHttpUrl().host)
.build()
}
private fun Request.Builder.addHeaders(headers: Map<String, Provider<String>>): Request.Builder {
headers.forEach { (name, valueProvider) ->
addHeader(name = name, value = valueProvider())
}
return this
}
}

View file

@ -14,8 +14,8 @@ sealed class ApiConfig {
/** Default environment */
abstract val defaultEnvironment: ApiEnvironment
/** Available environments with base url */
abstract val environments: Map<ApiEnvironment, String>
/** Available environments */
abstract val environmentConfigs: List<ApiEnvironmentConfig>
/** Unique id */
val id: ID = initializeId()

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.api.common.config
import com.tangem.utils.Provider
/**
* Api environment config
*
* @property environment environment
* @property baseUrl base url
* @property headers headers
*
[REDACTED_AUTHOR]
*/
data class ApiEnvironmentConfig(
val environment: ApiEnvironment,
val baseUrl: String,
val headers: Map<String, Provider<String>> = emptyMap(),
)

View file

@ -1,18 +1,50 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.utils.RequestHeader
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.utils.Provider
import com.tangem.utils.version.AppVersionProvider
/** Express [ApiConfig] */
internal class Express : ApiConfig() {
internal class Express(
private val expressAuthProvider: ExpressAuthProvider,
private val appVersionProvider: AppVersionProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environments: Map<ApiEnvironment, String> = mapOf(
ApiEnvironment.DEV to "[REDACTED_ENV_URL]",
ApiEnvironment.STAGE to "[REDACTED_ENV_URL]",
ApiEnvironment.PROD to "https://express.tangem.com/v1/",
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createDevEnvironment(),
createStageEnvironment(),
createProdEnvironment(),
)
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://express.tangem.com/v1/",
headers = createHeaders(),
)
private fun createHeaders() = buildMap {
put(key = "api-key", value = Provider(expressAuthProvider::getApiKey))
put(key = "user-id", value = Provider(expressAuthProvider::getUserId))
put(key = "session-id", value = Provider(expressAuthProvider::getSessionId))
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values)
}
private companion object {
fun getInitialEnvironment(): ApiEnvironment {

View file

@ -5,8 +5,20 @@ internal class TangemTech : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
override val environments: Map<ApiEnvironment, String> = mapOf(
ApiEnvironment.DEV to "https://devapi.tangem-tech.com/v1/",
ApiEnvironment.PROD to "https://api.tangem-tech.com/v1/",
override val environmentConfigs = listOf(
createDevEnvironment(),
createProdEnvironment(),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem-tech.com/v1/",
headers = emptyMap(), // TODO: [REDACTED_JIRA]
)
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "https://devapi.tangem-tech.com/v1/",
headers = emptyMap(), // TODO: [REDACTED_JIRA]
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.api.common.config.managers
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
/**
* Api configs manager
@ -12,6 +13,6 @@ interface ApiConfigsManager {
/** Initialize resources */
suspend fun initialize() {}
/** Get base url of api by [id] */
fun getBaseUrl(id: ApiConfig.ID): String
/** Get environment config by [id] */
fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig
}

View file

@ -3,6 +3,7 @@ package com.tangem.datasource.api.common.config.managers
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectMap
@ -44,7 +45,7 @@ internal class DevApiConfigsManager(
.launchIn(CoroutineScope(dispatchers.main))
}
override fun getBaseUrl(id: ApiConfig.ID): String {
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
val apiConfigs = _apiConfigs.value
val config = apiConfigs.map { it }.firstOrNull { it.key.id == id }?.key
@ -53,7 +54,7 @@ internal class DevApiConfigsManager(
val currentEnvironment = apiConfigs[config]
?: error("Current environment of api config with id [$id] not found")
return config.environments[currentEnvironment]
return config.environmentConfigs.firstOrNull { it.environment == currentEnvironment }
?: error("Api config with id [$id] doesn't contain environment [$currentEnvironment]")
}

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config.managers
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
/**
* Implementation of [ApiConfigsManager] in PROD environment
@ -12,14 +13,14 @@ internal class ProdApiConfigsManager(
private val apiConfigs: ApiConfigs,
) : ApiConfigsManager {
override fun getBaseUrl(id: ApiConfig.ID): String {
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
val config = apiConfigs.firstOrNull { it.id == id }
?: error("Api config with id [$id] not found. Check ApiConfig implementations")
?: error("Api config with id [$id] not found. Check that ApiConfig with id [$id] was provided into DI")
return config.environments[config.defaultEnvironment]
return config.environmentConfigs.firstOrNull { it.environment == config.defaultEnvironment }
?: error(
"Api config with id [$id] doesn't contain " +
"environment [${config.defaultEnvironment}]. Check ApiConfig implementations",
"Api config with id [$id] doesn't contain environment [${config.defaultEnvironment}]. " +
"Check ApiConfig's environments is included default environment",
)
}
}

View file

@ -3,6 +3,8 @@ package com.tangem.datasource.di
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.Express
import com.tangem.datasource.api.common.config.TangemTech
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.utils.version.AppVersionProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -15,7 +17,12 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideExpressConfig(): ApiConfig = Express()
fun provideExpressConfig(
expressAuthProvider: ExpressAuthProvider,
appVersionProvider: AppVersionProvider,
): ApiConfig {
return Express(expressAuthProvider, appVersionProvider)
}
@Provides
@IntoSet

View file

@ -21,7 +21,6 @@ import com.tangem.datasource.utils.RequestHeader.*
import com.tangem.datasource.utils.addEnvironmentSwitcher
import com.tangem.datasource.utils.addHeaders
import com.tangem.datasource.utils.addLoggers
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.version.AppVersionProvider
@ -59,19 +58,18 @@ class NetworkModule {
fun provideExpressApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
expressAuthProvider: ExpressAuthProvider,
appVersionProvider: AppVersionProvider,
apiConfigsManager: ApiConfigsManager,
): TangemExpressApi {
val environmentConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.Express)
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.baseUrl(apiConfigsManager.getBaseUrl(id = ApiConfig.ID.Express))
.baseUrl(environmentConfig.baseUrl)
.client(
OkHttpClient.Builder()
.addEnvironmentSwitcher(ApiConfig.ID.Express, apiConfigsManager)
.addHeaders(Express(expressAuthProvider))
.addHeaders(AppVersionPlatformHeaders(appVersionProvider))
.addHeaders(environmentConfig.headers)
.addLoggers(context)
.build(),
)
@ -113,7 +111,7 @@ class NetworkModule {
context = context,
appVersionProvider = appVersionProvider,
apiConfigsManager = apiConfigsManager,
baseUrl = apiConfigsManager.getBaseUrl(id = ApiConfig.ID.TangemTech),
baseUrl = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemTech).baseUrl,
)
}
@ -165,7 +163,7 @@ class NetworkModule {
context = context,
appVersionProvider = appVersionProvider,
apiConfigsManager = apiConfigsManager,
baseUrl = apiConfigsManager.getBaseUrl(id = ApiConfig.ID.TangemTech),
baseUrl = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemTech).baseUrl,
timeouts = Timeouts(
callTimeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS,
),

View file

@ -3,10 +3,11 @@ package com.tangem.datasource.utils
import android.content.Context
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.SwitchBaseUrlInterceptor
import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.utils.Provider
import okhttp3.Interceptor
import okhttp3.OkHttpClient
@ -16,7 +17,7 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade
Interceptor { chain ->
val request = chain.request().newBuilder().apply {
requestHeaders
.flatMap(RequestHeader::values)
.flatMap { it.values.toList() }
.forEach { addHeader(it.first, it.second.invoke()) }
}.build()
@ -25,6 +26,19 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade
)
}
/** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */
internal fun OkHttpClient.Builder.addHeaders(requestHeaders: Map<String, Provider<String>>): OkHttpClient.Builder {
return addInterceptor(
Interceptor { chain ->
val request = chain.request().newBuilder().apply {
requestHeaders.forEach { addHeader(it.key, it.value.invoke()) }
}.build()
chain.proceed(request)
},
)
}
/**
* Extension for logging each [OkHttpClient] request
*
@ -53,7 +67,7 @@ internal fun OkHttpClient.Builder.addEnvironmentSwitcher(
): OkHttpClient.Builder {
return if (BuildConfig.TESTER_MENU_ENABLED) {
addInterceptor(
interceptor = SwitchBaseUrlInterceptor(
interceptor = SwitchEnvironmentInterceptor(
id = id,
apiConfigsManager = apiConfigsManager,
),

View file

@ -1,8 +1,8 @@
package com.tangem.datasource.utils
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.utils.Provider
import com.tangem.utils.version.AppVersionProvider
/**
@ -10,31 +10,25 @@ import com.tangem.utils.version.AppVersionProvider
*
* @param pairs header name and header value pairs
*/
sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
sealed class RequestHeader(vararg pairs: Pair<String, Provider<String>>) {
/** Header list */
val values: List<Pair<String, () -> String>> = pairs.toList()
val values: Map<String, Provider<String>> = pairs.toMap()
data object CacheControlHeader : RequestHeader("Cache-Control" to { "max-age=600" })
data object CacheControlHeader : RequestHeader("Cache-Control" to Provider { "max-age=600" })
class AuthenticationHeader(authProvider: AuthProvider) : RequestHeader(
"card_id" to { authProvider.getCardId() },
"card_public_key" to { authProvider.getCardPublicKey() },
)
class Express(expressAuthProvider: ExpressAuthProvider) : RequestHeader(
"api-key" to { expressAuthProvider.getApiKey() },
"user-id" to { expressAuthProvider.getUserId() },
"session-id" to { expressAuthProvider.getSessionId() },
"card_id" to Provider(authProvider::getCardId),
"card_public_key" to Provider(authProvider::getCardPublicKey),
)
class AppVersionPlatformHeaders(appVersionProvider: AppVersionProvider) : RequestHeader(
"version" to { appVersionProvider.versionName },
"platform" to { "android" },
"version" to Provider(appVersionProvider::versionName),
"platform" to Provider { "android" },
)
class StakeKit(stakeKitAuthProvider: StakeKitAuthProvider) : RequestHeader(
"X-API-KEY" to { stakeKitAuthProvider.getApiKey() },
"accept" to { "application/json" },
"X-API-KEY" to Provider(stakeKitAuthProvider::getApiKey),
"accept" to Provider { "application/json" },
)
}

View file

@ -10,6 +10,7 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
import com.tangem.datasource.api.common.config.Express
import com.tangem.datasource.api.common.config.TangemTech
import io.mockk.mockk
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@ -24,7 +25,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
@Test
fun test_getBaseUrl() {
val actual = manager.getBaseUrl(id = model.id)
val actual = manager.getEnvironmentConfig(id = model.id).baseUrl
Truth.assertThat(actual).isEqualTo(model.expected)
}
@ -34,7 +35,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
private companion object {
val API_CONFIGS = setOf(
Express(),
Express(mockk(), mockk()),
TangemTech(),
// Don't forget to add new config

View file

@ -69,10 +69,10 @@ internal class EnvironmentsTogglesViewModel @Inject constructor(
EnvironmentTogglesScreenUM.ApiInfoUM(
name = config.id.name,
select = currentEnvironment.name,
url = config.environments[currentEnvironment]
url = config.environmentConfigs.firstOrNull { it.environment == currentEnvironment }?.baseUrl
?: error("Current environment's url isn't found"),
environments = config.environments
.map { environment -> environment.key.name }
environments = config.environmentConfigs
.map { environmentConfig -> environmentConfig.environment.name }
.toImmutableSet(),
)
}