Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-15 10:33:05 +03:00
parent 7d9c12f1bc
commit 690318cd8d
10 changed files with 80 additions and 65 deletions

View file

@ -2,7 +2,8 @@ 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 com.tangem.utils.ProviderSuspend
import kotlinx.coroutines.runBlocking
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
@ -44,11 +45,13 @@ internal class SwitchEnvironmentInterceptor(
.build()
}
private fun Request.Builder.addHeaders(headers: Map<String, Provider<String>>): Request.Builder {
headers.forEach { (name, valueProvider) ->
val value = valueProvider()
private fun Request.Builder.addHeaders(headers: Map<String, ProviderSuspend<String>>): Request.Builder {
runBlocking {
headers.forEach { (name, valueProvider) ->
val value = valueProvider()
if (value.isNotBlank()) addHeader(name = name, value = value)
if (value.isNotBlank()) addHeader(name = name, value = value)
}
}
return this

View file

@ -1,6 +1,6 @@
package com.tangem.datasource.api.common.config
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
/**
* Api environment config
@ -14,5 +14,5 @@ import com.tangem.utils.Provider
data class ApiEnvironmentConfig(
val environment: ApiEnvironment,
val baseUrl: String,
val headers: Map<String, Provider<String>> = emptyMap(),
val headers: Map<String, ProviderSuspend<String>> = emptyMap(),
)

View file

@ -4,7 +4,7 @@ import com.tangem.datasource.BuildConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.utils.RequestHeader
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider
/**
@ -47,11 +47,11 @@ internal class Express(
)
private fun createHeaders(isProd: Boolean) = buildMap {
put(key = "api-key", value = Provider { getApiKey(isProd) })
put(key = "user-id", value = Provider(expressAuthProvider::getUserId))
put(key = "session-id", value = Provider(expressAuthProvider::getSessionId))
put(key = "api-key", value = ProviderSuspend { getApiKey(isProd) })
put(key = "user-id", value = ProviderSuspend(expressAuthProvider::getUserId))
put(key = "session-id", value = ProviderSuspend(expressAuthProvider::getSessionId))
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values)
put(key = "refcode", value = Provider(expressAuthProvider::getRefCode))
put(key = "refcode", value = ProviderSuspend(expressAuthProvider::getRefCode))
}
private fun getApiKey(isProd: Boolean): String {

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.api.common.config
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
/**
* StakeKit [ApiConfig]
@ -25,8 +25,8 @@ internal class StakeKit(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.stakek.it/v1/",
headers = mapOf(
"X-API-KEY" to Provider(stakeKitAuthProvider::getApiKey),
"accept" to Provider { "application/json" },
"X-API-KEY" to ProviderSuspend(stakeKitAuthProvider::getApiKey),
"accept" to ProviderSuspend { "application/json" },
),
)
}

View file

@ -1,8 +1,7 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.utils.Provider
import kotlinx.coroutines.runBlocking
import com.tangem.utils.ProviderSuspend
internal class TangemVisa(
private val authProvider: TangemVisaAuthProvider,
@ -21,10 +20,6 @@ internal class TangemVisa(
)
private fun createHeaders() = mapOf(
"Authorization" to Provider {
// This is safe because it's used by interceptor which runs on the IO thread
// (maybe change to ProviderSuspend implementation)
runBlocking { authProvider.getAuthHeader() }
},
"Authorization" to ProviderSuspend { authProvider.getAuthHeader() },
)
}

View file

@ -1,6 +1,6 @@
package com.tangem.datasource.api.common.config
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
internal class TangemVisaAuth : ApiConfig() {
@ -16,5 +16,5 @@ internal class TangemVisaAuth : ApiConfig() {
headers = createHeaders(),
)
private fun createHeaders() = mapOf<String, Provider<String>>()
private fun createHeaders() = mapOf<String, ProviderSuspend<String>>()
}

View file

@ -10,7 +10,8 @@ import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.api.utils.ConnectTimeout
import com.tangem.datasource.api.utils.ReadTimeout
import com.tangem.datasource.api.utils.WriteTimeout
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Invocation
@ -22,7 +23,7 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade
val request = chain.request().newBuilder().apply {
requestHeaders
.flatMap { it.values.toList() }
.forEach { addHeader(it.first, it.second.invoke()) }
.forEach { addHeader(it.first, runBlocking { it.second.invoke() }) }
}.build()
chain.proceed(request)
@ -59,14 +60,17 @@ internal fun OkHttpClient.Builder.applyTimeoutAnnotations(): OkHttpClient.Builde
}
/** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */
internal fun OkHttpClient.Builder.addHeaders(requestHeaders: Map<String, Provider<String>>): OkHttpClient.Builder {
internal fun OkHttpClient.Builder.addHeaders(
requestHeaders: Map<String, ProviderSuspend<String>>,
): OkHttpClient.Builder {
return addInterceptor(
Interceptor { chain ->
val request = chain.request().newBuilder().apply {
requestHeaders.forEach {
val value = it.value.invoke()
if (value.isNotBlank()) addHeader(name = it.key, value = value)
runBlocking {
requestHeaders.forEach {
val value = it.value.invoke()
if (value.isNotBlank()) addHeader(name = it.key, value = value)
}
}
}.build()

View file

@ -3,7 +3,7 @@ package com.tangem.datasource.utils
import android.os.Build
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.utils.RequestHeader.CacheControlHeader.checkHeaderValueOrEmpty
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider
import java.util.Locale
import java.util.TimeZone
@ -13,26 +13,26 @@ import java.util.TimeZone
*
* @param pairs header name and header value pairs
*/
sealed class RequestHeader(vararg pairs: Pair<String, Provider<String>>) {
sealed class RequestHeader(vararg pairs: Pair<String, ProviderSuspend<String>>) {
/** Header list */
val values: Map<String, Provider<String>> = pairs.toMap()
val values: Map<String, ProviderSuspend<String>> = pairs.toMap()
data object CacheControlHeader : RequestHeader("Cache-Control" to Provider { "max-age=600" })
data object CacheControlHeader : RequestHeader("Cache-Control" to ProviderSuspend { "max-age=600" })
class AuthenticationHeader(authProvider: AuthProvider) : RequestHeader(
"card_id" to Provider(authProvider::getCardId),
"card_public_key" to Provider(authProvider::getCardPublicKey),
"card_id" to ProviderSuspend(authProvider::getCardId),
"card_public_key" to ProviderSuspend(authProvider::getCardPublicKey),
)
class AppVersionPlatformHeaders(appVersionProvider: AppVersionProvider) : RequestHeader(
"version" to Provider(appVersionProvider::versionName),
"platform" to Provider { "android" },
"language" to Provider { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"timezone" to Provider {
"version" to ProviderSuspend { appVersionProvider.versionName },
"platform" to ProviderSuspend { "android" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},
"device" to Provider { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
"device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
)
/**

View file

@ -13,8 +13,9 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
@ -37,6 +38,8 @@ private val API_CONFIGS = setOf(
Express(configManager, expressAuthProvider, appVersionProvider),
TangemTech(appVersionProvider, appAuthProvider),
StakeKit(stakeKitAuthProvider),
TangemVisaAuth(),
TangemVisa(visaAuthProvider),
)
/**
@ -56,7 +59,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
every { appAuthProvider.getCardId() } returns APP_CARD_ID
every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
every { runBlocking { visaAuthProvider.getAuthHeader() } } returns VISA_AUTH_HEADER // TODO
coEvery { visaAuthProvider.getAuthHeader() } returns VISA_AUTH_HEADER
}
@Test
@ -66,8 +69,8 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
Truth.assertThat(actual.environment).isEqualTo(model.expected.environment)
Truth.assertThat(actual.baseUrl).isEqualTo(model.expected.baseUrl)
Truth.assertThat(actual.headers.mapValues { it.value() })
.isEqualTo(model.expected.headers.mapValues { it.value() })
Truth.assertThat(actual.headers.mapValues { runBlocking { it.value() } })
.isEqualTo(model.expected.headers.mapValues { runBlocking { it.value() } })
}
data class Model(val id: ApiConfig.ID, val expected: ApiEnvironmentConfig)
@ -122,23 +125,23 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
},
headers = mapOf(
"api-key" to Provider {
"api-key" to ProviderSuspend {
if (environment == ApiEnvironment.PROD) {
MockEnvironmentConfigStorage.EXPRESS_API_KEY
} else {
MockEnvironmentConfigStorage.EXPRESS_DEV_API_KEY
}
},
"user-id" to Provider { EXPRESS_USER_ID },
"session-id" to Provider { EXPRESS_SESSION_ID },
"refcode" to Provider { EXPRESS_REF_CODE },
"version" to Provider { VERSION_NAME },
"platform" to Provider { "android" },
"language" to Provider { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"timezone" to Provider {
"user-id" to ProviderSuspend { EXPRESS_USER_ID },
"session-id" to ProviderSuspend { EXPRESS_SESSION_ID },
"refcode" to ProviderSuspend { EXPRESS_REF_CODE },
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "android" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},
"device" to Provider { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
"device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
),
),
)
@ -151,15 +154,15 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem-tech.com/v1/",
headers = mapOf(
"card_id" to Provider { APP_CARD_ID },
"card_public_key" to Provider { APP_CARD_PUBLIC_KEY },
"version" to Provider { VERSION_NAME },
"platform" to Provider { "android" },
"language" to Provider { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"timezone" to Provider {
"card_id" to ProviderSuspend { APP_CARD_ID },
"card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY },
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "android" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
},
"device" to Provider { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
"device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
),
),
)
@ -172,8 +175,8 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
environment = ApiEnvironment.PROD,
baseUrl = "https://api.stakek.it/v1/",
headers = mapOf(
"X-API-KEY" to Provider { STAKE_KIT_API_KEY },
"accept" to Provider { "application/json" },
"X-API-KEY" to ProviderSuspend { STAKE_KIT_API_KEY },
"accept" to ProviderSuspend { "application/json" },
),
),
)
@ -196,7 +199,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
environment = ApiEnvironment.PROD,
baseUrl = "https://bff.tangem.com/",
headers = mapOf(
"Authorization" to Provider { VISA_AUTH_HEADER },
"Authorization" to ProviderSuspend { VISA_AUTH_HEADER },
),
),
)

View file

@ -0,0 +1,10 @@
package com.tangem.utils
/**
* Provider for suspend lazy initialization
*
* @param action initialization action
*
[REDACTED_AUTHOR]
*/
class ProviderSuspend<T>(action: suspend () -> T) : suspend () -> T by action