Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-09 11:08:16 +03:00
parent 2a835ce3c9
commit 0c56df0075
16 changed files with 306 additions and 58 deletions

View file

@ -16,6 +16,9 @@ enum class ApiEnvironment {
@Json(name = "STAGE")
STAGE,
@Json(name = "MOCK")
MOCK,
@Json(name = "PROD")
PROD,
}

View file

@ -14,6 +14,7 @@ import com.tangem.utils.version.AppVersionProvider
* @property environmentConfigStorage environment config storage
* @property expressAuthProvider express auth provider
* @property appVersionProvider app version provider
* @property appInfoProvider app info provider
*/
internal class Express(
private val environmentConfigStorage: EnvironmentConfigStorage,
@ -27,9 +28,25 @@ internal class Express(
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createDevEnvironment(),
createStageEnvironment(),
createMockedEnvironment(),
createProdEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
DEBUG_BUILD_TYPE,
-> ApiEnvironment.DEV
INTERNAL_BUILD_TYPE,
-> ApiEnvironment.STAGE
MOCKED_BUILD_TYPE,
-> ApiEnvironment.MOCK
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
@ -42,6 +59,12 @@ internal class Express(
headers = createHeaders(isProd = false),
)
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(isProd = false),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://express.tangem.com/v1/",
@ -63,21 +86,4 @@ internal class Express(
?.apiKey
?: error("No express config provided")
}
private companion object {
fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
DEBUG_BUILD_TYPE,
-> ApiEnvironment.DEV
INTERNAL_BUILD_TYPE,
MOCKED_BUILD_TYPE,
-> ApiEnvironment.STAGE
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider
@ -7,11 +8,37 @@ internal class TangemPay(
private val appVersionProvider: AppVersionProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.DEV
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs = listOf(
createProdEnvironment(),
createDevEnvironment(),
createMockedEnvironment(),
createProdEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
-> ApiEnvironment.DEV
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
@ -20,12 +47,6 @@ internal class TangemPay(
headers = createHeaders(),
)
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createHeaders() = mapOf(
"version" to ProviderSuspend { appVersionProvider.versionName },
"platform" to ProviderSuspend { "Android" },

View file

@ -1,5 +1,6 @@
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.info.AppInfoProvider
@ -12,14 +13,28 @@ internal class TangemTech(
private val appInfoProvider: AppInfoProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs = listOf(
createDevEnvironment(),
createStageEnvironment(),
createMockedEnvironment(),
createProdEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
-> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
@ -32,6 +47,12 @@ internal class TangemTech(
headers = createHeaders(),
)
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem.org/v1/",

View file

@ -20,28 +20,26 @@ import kotlinx.coroutines.flow.*
* @property dispatchers coroutine dispatcher provider
*/
internal class DevApiConfigsManager(
apiConfigs: ApiConfigs,
private val apiConfigs: ApiConfigs,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : MutableApiConfigsManager {
override val configs: Flow<Map<ApiConfig, ApiEnvironment>> get() = _apiConfigs
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
field = MutableStateFlow(value = getInitialConfigs())
private val _apiConfigs = MutableStateFlow(value = apiConfigs.associateWith { it.defaultEnvironment })
override val isInitialized: StateFlow<Boolean> get() = _isInitialized.asStateFlow()
private val _isInitialized = MutableStateFlow(value = false)
override val isInitialized: StateFlow<Boolean>
field = MutableStateFlow(value = false)
override fun initialize() {
_isInitialized.value = false
isInitialized.value = false
// We can't use appPreferencesStore.getObjectMap as base flow,
// because we should keep possibility to work with configs synchronous.
// See [getBaseUrl]
appPreferencesStore.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
.onEach { savedEnvironments ->
_apiConfigs.update { apiConfigs ->
configs.update { apiConfigs ->
apiConfigs.mapValues {
val (config, currentEnvironment) = it
@ -49,15 +47,15 @@ internal class DevApiConfigsManager(
}
}
if (!_isInitialized.value) {
_isInitialized.value = true
if (!isInitialized.value) {
isInitialized.value = true
}
}
.launchIn(CoroutineScope(SupervisorJob() + dispatchers.default))
}
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
val apiConfigs = _apiConfigs.value
val apiConfigs = configs.value
val config = apiConfigs.map { it }.firstOrNull { it.key.id == id }?.key
?: error("Api config with id [$id] not found")
@ -78,4 +76,27 @@ internal class DevApiConfigsManager(
mutablePreferences.setObjectMap(PreferencesKeys.apiConfigsEnvironmentKey, updatedMap)
}
}
override suspend fun changeEnvironment(environment: ApiEnvironment) {
val supportedConfigs = apiConfigs
.filter { config ->
config.environmentConfigs.any { it.environment == environment }
}
.map { it.id.name }
appPreferencesStore.editData { mutablePreferences ->
val updatedMap = mutablePreferences.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
.mapValues { (configName, currentEnvironment) ->
val isEnvSupported = supportedConfigs.contains(configName)
if (isEnvSupported) environment else currentEnvironment
}
mutablePreferences.setObjectMap(key = PreferencesKeys.apiConfigsEnvironmentKey, value = updatedMap)
}
}
private fun getInitialConfigs(): Map<ApiConfig, ApiEnvironment> {
return apiConfigs.associateWith(ApiConfig::defaultEnvironment)
}
}

View file

@ -0,0 +1,61 @@
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 kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
/**
* Implementation of [ApiConfigsManager] in MOCK environment
*
* @property apiConfigs api configs
*
[REDACTED_AUTHOR]
*/
internal class MockApiConfigsManager(
private val apiConfigs: ApiConfigs,
) : MutableApiConfigsManager {
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
field = MutableStateFlow(value = getInitialConfigs())
override val isInitialized: StateFlow<Boolean> = MutableStateFlow(value = true)
override fun initialize() = Unit
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
val apiConfigs = configs.value
val (config, currentEnvironment) = apiConfigs.entries.firstOrNull { it.key.id == id }
?: error("Api config with id [$id] not found")
return config.environmentConfigs.firstOrNull { it.environment == currentEnvironment }
?: error("Api config with id [$id] doesn't contain environment [$currentEnvironment]")
}
override suspend fun changeEnvironment(id: String, environment: ApiEnvironment) {
configs.update { apiConfigs ->
val apiConfig = apiConfigs.keys.firstOrNull { it.id.name == id }
?: error("Api config with id [$id] not found. Check that ApiConfig with id [$id] was provided into DI")
apiConfigs + (apiConfig to environment)
}
}
override suspend fun changeEnvironment(environment: ApiEnvironment) {
configs.update { apiConfigs ->
apiConfigs.mapValues { (config, currentEnvironment) ->
val isEnvSupported = config.environmentConfigs.any { it.environment == environment }
if (isEnvSupported) environment else currentEnvironment
}
}
}
private fun getInitialConfigs(): Map<ApiConfig, ApiEnvironment> {
return apiConfigs.associateWith { it.defaultEnvironment }
}
}

View file

@ -16,4 +16,7 @@ interface MutableApiConfigsManager : ApiConfigsManager {
/** Change api environment [environment] by [id] */
suspend fun changeEnvironment(id: String, environment: ApiEnvironment)
/** Change api environment [environment] for all configs */
suspend fun changeEnvironment(environment: ApiEnvironment)
}

View file

@ -6,18 +6,20 @@ import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.blockaid.BlockAidApi
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager
import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager
import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechApiV2
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.utils.*
@ -45,7 +47,7 @@ internal object NetworkModule {
private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L
private val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
// ApiConfig.ID.StakeKit,
ApiConfig.ID.StakeKit,
)
@Provides
@ -55,10 +57,10 @@ internal object NetworkModule {
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): ApiConfigsManager {
return if (BuildConfig.TESTER_MENU_ENABLED) {
DevApiConfigsManager(apiConfigs, appPreferencesStore, dispatchers)
} else {
ProdApiConfigsManager(apiConfigs)
return when {
BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE -> MockApiConfigsManager(apiConfigs)
BuildConfig.TESTER_MENU_ENABLED -> DevApiConfigsManager(apiConfigs, appPreferencesStore, dispatchers)
else -> ProdApiConfigsManager(apiConfigs)
}
}

View file

@ -5,6 +5,7 @@ import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.api.utils.ConnectTimeout
@ -93,7 +94,7 @@ internal fun OkHttpClient.Builder.applyApiConfig(
id: ApiConfig.ID,
apiConfigsManager: ApiConfigsManager,
): OkHttpClient.Builder {
return if (BuildConfig.TESTER_MENU_ENABLED) {
return if (BuildConfig.TESTER_MENU_ENABLED || BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE) {
addInterceptor(
interceptor = SwitchEnvironmentInterceptor(
id = id,