Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-09 16:09:24 +03:00
commit ff09b9f79b
361 changed files with 10298 additions and 8064 deletions

View file

@ -0,0 +1,43 @@
package com.tangem.datasource.api.common
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Response
import okio.IOException
/**
* Switch base url [Interceptor]
*
* @property id api config id [ApiConfig.ID]
* @property apiConfigsManager api configs manager
*
[REDACTED_AUTHOR]
*/
internal class SwitchBaseUrlInterceptor(
private val id: ApiConfig.ID,
private val apiConfigsManager: ApiConfigsManager,
) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val builder = request.newBuilder()
request = builder
.url(url = request.url.adjustBaseUrl())
.build()
return chain.proceed(request)
}
private fun HttpUrl.adjustBaseUrl(): HttpUrl {
val host = apiConfigsManager.getBaseUrl(id).toHttpUrl().host
return this.newBuilder()
.host(host)
.build()
}
}

View file

@ -0,0 +1,50 @@
package com.tangem.datasource.api.common.config
/**
* Api config
*
* @property currentEnvironment current api environment
*
* @see <a href="https://www.notion.so/tangem/API-eacb264e7daf420a88b419a8a26f5b26?pvs=4">API configuration</a>
*
[REDACTED_AUTHOR]
*/
sealed class ApiConfig(open val currentEnvironment: ApiEnvironment) {
/** Available environments with base url */
abstract val environments: Map<ApiEnvironment, String>
/** Unique id */
val id: ID = initializeId()
enum class ID {
Express,
TangemTech,
}
/** Copy method for sealed class [ApiConfig] */
fun copySealed(currentEnvironment: ApiEnvironment): ApiConfig {
return when (this) {
is Express -> copy(currentEnvironment = currentEnvironment)
is TangemTech -> copy(currentEnvironment = currentEnvironment)
}
}
private fun initializeId(): ID {
return when (this) {
is Express -> ID.Express
is TangemTech -> ID.TangemTech
}
}
companion object {
internal const val DEBUG_BUILD_TYPE = "debug"
internal const val INTERNAL_BUILD_TYPE = "internal"
internal const val MOCKED_BUILD_TYPE = "mocked"
internal const val EXTERNAL_BUILD_TYPE = "external"
internal const val RELEASE_BUILD_TYPE = "release"
/** All api configs */
fun values() = listOf(Express(), TangemTech())
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.common.config
/**
* Api environment
*
[REDACTED_AUTHOR]
*/
enum class ApiEnvironment {
DEV, STAGE, PROD
}

View file

@ -0,0 +1,35 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
/**
* Express [ApiConfig]
*
* @property currentEnvironment current api environment
*/
internal data class Express(
override val currentEnvironment: ApiEnvironment = initializeCurrentEnvironment(),
) : ApiConfig(currentEnvironment) {
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/",
)
private companion object {
fun initializeCurrentEnvironment(): 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

@ -0,0 +1,34 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
/**
* TangemTech [ApiConfig]
*
* @property currentEnvironment current api environment
*/
internal data class TangemTech(
override val currentEnvironment: ApiEnvironment = initializeCurrentEnvironment(),
) : ApiConfig(currentEnvironment) {
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/",
)
private companion object {
fun initializeCurrentEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
-> ApiEnvironment.DEV
MOCKED_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
}
}

View file

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

View file

@ -0,0 +1,69 @@
package com.tangem.datasource.api.common.config.managers
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
/**
* Implementation of [ApiConfigsManager] in DEV environment
*
* @property appPreferencesStore app preferences store
* @property dispatchers coroutine dispatcher provider
*/
internal class DevApiConfigsManager(
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : MutableApiConfigsManager {
override val configs: Flow<List<ApiConfig>> get() = _apiConfigs
private val _apiConfigs = MutableStateFlow(value = ApiConfig.values())
override suspend fun initialize() {
// 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.value = _apiConfigs.value.map { config ->
val savedEnvironment = savedEnvironments[config.id.name]
if (savedEnvironment != null) {
config.copySealed(currentEnvironment = savedEnvironment)
} else {
config
}
}
}
.launchIn(CoroutineScope(dispatchers.main))
}
override fun getBaseUrl(id: ApiConfig.ID): String {
val config = _apiConfigs.value.firstOrNull { it.id == id }
?: error("Api config with id [$id] not found. Check ApiConfig implementations")
return config.environments[config.currentEnvironment]
?: error(
"Api config with id [$id] doesn't contain environment [${config.currentEnvironment}]. " +
"Check ApiConfig implementations",
)
}
override suspend fun changeEnvironment(id: String, environment: ApiEnvironment) {
appPreferencesStore.editData { mutablePreferences ->
val updatedMap = mutablePreferences.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
.toMutableMap()
.apply { put(id, environment) }
mutablePreferences.setObjectMap(PreferencesKeys.apiConfigsEnvironmentKey, updatedMap)
}
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.datasource.api.common.config.managers
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import kotlinx.coroutines.flow.Flow
/**
* Mutable [ApiConfigsManager] for change information about the current api environment
*
[REDACTED_AUTHOR]
*/
interface MutableApiConfigsManager : ApiConfigsManager {
/** Configs */
val configs: Flow<List<ApiConfig>>
/** Change api environment [environment] by [id] */
suspend fun changeEnvironment(id: String, environment: ApiEnvironment)
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.api.common.config.managers
import com.tangem.datasource.api.common.config.ApiConfig
/** Implementation of [ApiConfigsManager] in PROD environment */
internal class ProdApiConfigsManager : ApiConfigsManager {
override fun getBaseUrl(id: ApiConfig.ID): String {
val config = ApiConfig.values().firstOrNull { it.id == id }
?: error("Api config with id [$id] not found. Check ApiConfig implementations")
return config.environments[config.currentEnvironment]
?: error(
"Api config with id [$id] doesn't contain " +
"environment [${config.currentEnvironment}]. Check ApiConfig implementations",
)
}
}

View file

@ -16,18 +16,20 @@ interface TangemTechMarketsApi {
@Query("offset") offset: Int,
@Query("limit") limit: Int,
@Query("order") order: String,
@Query("general_coins") generalCoins: Boolean,
@Query("search") search: String?,
@Query("timestamp") timestamp: Long?,
): ApiResponse<TokenMarketListResponse>
@GET("coins/{coin_id}")
suspend fun getCoinMarketData(
@Path("coin_id") coinId: String,
@Query("currency") currency: String,
): ApiResponse<TokenMarketDetailsResponse>
@Query("language") language: String,
): ApiResponse<TokenMarketInfoResponse>
@GET("coins/{coin_id}/history")
suspend fun getCoinChart(
@Path("coin_id") coinId: String,
@Query("currency") currency: String,
@Query("interval") interval: String,
): ApiResponse<TokenMarketChartResponse>

View file

@ -3,130 +3,129 @@ package com.tangem.datasource.api.markets.models.response
import com.squareup.moshi.Json
import java.math.BigDecimal
data class TokenMarketDetailsResponse(
data class TokenMarketInfoResponse(
@Json(name = "id")
val id: String,
@Json(name = "name")
val name: String,
@Json(name = "symbol")
val symbol: String,
@Json(name = "active")
val active: Boolean,
@Json(name = "current_price")
val currentPrice: BigDecimal,
@Json(name = "price_change_percentage")
val priceChangePercentage: PriceChangePercentage,
val priceChangePercentage: PriceChangePercentage?,
@Json(name = "networks")
val networks: List<Network>,
val networks: List<Network>?,
@Json(name = "short_description")
val shortDescription: String?,
@Json(name = "full_description")
val fullDescription: String?,
@Json(name = "insights")
val insights: List<Insight>?,
val insights: Insights?,
@Json(name = "metrics")
val metrics: Metrics,
val metrics: Metrics?,
@Json(name = "links")
val links: Links,
val links: Links?,
@Json(name = "price_performance")
val pricePerformance: PricePerformance,
val pricePerformance: PricePerformance?,
) {
data class PriceChangePercentage(
@Json(name = "24h")
val h24: BigDecimal,
val day: BigDecimal?,
@Json(name = "1w")
val week1: BigDecimal,
val week: BigDecimal?,
@Json(name = "1m")
val month1: BigDecimal,
val month: BigDecimal?,
@Json(name = "3m")
val month3: BigDecimal,
val threeMonths: BigDecimal?,
@Json(name = "6m")
val month6: BigDecimal,
val sixMonths: BigDecimal?,
@Json(name = "1y")
val year1: BigDecimal,
val year: BigDecimal?,
@Json(name = "all_time")
val allTime: BigDecimal,
val allTime: BigDecimal?,
)
data class Network(
@Json(name = "network_id")
val networkId: String,
@Json(name = "exchangeable")
val exchangeable: Boolean,
val exchangeable: Boolean = false,
@Json(name = "contract_address")
val contractAddress: String,
@Json(name = "decimalCount")
val decimalCount: Int,
val contractAddress: String?,
@Json(name = "decimal_count")
val decimalCount: Int?,
)
data class Insight(
data class Insights(
@Json(name = "holders_change")
val holdersChange: Change,
val holdersChange: Change?,
@Json(name = "liquidity_change")
val liquidityChange: Change,
val liquidityChange: Change?,
@Json(name = "buy_pressure_change")
val buyPressureChange: Change,
val buyPressureChange: Change?,
@Json(name = "experienced_buyer_change")
val experiencedBuyerChange: Change,
) {
data class Change(
@Json(name = "1d")
val day1: Int,
@Json(name = "1w")
val week1: Int,
@Json(name = "1m")
val month1: Int,
)
}
val experiencedBuyerChange: Change?,
)
data class Change(
@Json(name = "24h")
val day: BigDecimal?,
@Json(name = "1w")
val week: BigDecimal?,
@Json(name = "1m")
val month: BigDecimal?,
)
data class Metrics(
@Json(name = "market_rating")
val marketRating: Int,
val marketRating: Int?,
@Json(name = "circulating_supply")
val circulatingSupply: BigDecimal,
val circulatingSupply: BigDecimal?,
@Json(name = "market_cap")
val marketCap: BigDecimal,
val marketCap: BigDecimal?,
@Json(name = "volume_24h")
val volume24h: BigDecimal,
val volume24h: BigDecimal?,
@Json(name = "total_supply")
val totalSupply: BigDecimal,
val totalSupply: BigDecimal?,
@Json(name = "fully_diluted_valuation")
val fullyDilutedValuation: BigDecimal,
val fullyDilutedValuation: BigDecimal?,
)
data class Links(
@Json(name = "official_links")
val officialLinks: List<Link> = emptyList(),
val officialLinks: List<Link>?,
@Json(name = "social")
val social: List<Link> = emptyList(),
val social: List<Link>?,
@Json(name = "repository")
val repository: List<Link> = emptyList(),
val repository: List<Link>?,
@Json(name = "blockchain_site")
val blockchainSite: List<Link> = emptyList(),
val blockchainSite: List<Link>?,
)
data class Link(
@Json(name = "title")
val title: String?,
val title: String,
@Json(name = "id")
val id: String,
val id: String?,
@Json(name = "link")
val url: String,
val link: String,
)
data class PricePerformance(
@Json(name = "high_price")
val highPrice: Price,
@Json(name = "24h")
val day: Range?,
@Json(name = "1m")
val month: Range?,
@Json(name = "all_time")
val allTime: Range?,
)
data class Range(
@Json(name = "low_price")
val lowPrice: Price,
) {
data class Price(
@Json(name = "24h")
val h24: BigDecimal,
@Json(name = "1m")
val month1: BigDecimal,
@Json(name = "all_time")
val allTime: BigDecimal,
)
}
val low: BigDecimal?,
@Json(name = "high_price")
val high: BigDecimal?,
)
}

View file

@ -14,6 +14,8 @@ data class TokenMarketListResponse(
val limit: Int,
@Json(name = "offset")
val offset: Int,
@Json(name = "timestamp")
val timestamp: Long? = null,
) {
data class Token(
@Json(name = "id")

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class AddressArgumentDTO(
@ -10,7 +11,7 @@ data class AddressArgumentDTO(
@Json(name = "network")
val network: String? = null,
@Json(name = "minimum")
val minimum: Double? = null,
val minimum: BigDecimal? = null,
@Json(name = "maximum")
val maximum: Double? = null,
val maximum: BigDecimal? = null,
)

View file

@ -16,7 +16,7 @@ class StakeKitErrorResponse(
@Json(name = "code")
val code: String? = null,
@Json(name = "countryCode")
val countryCode: String,
val countryCode: String?,
@Json(name = "regionCode")
val regionCode: String? = null,
@Json(name = "tags")

View file

@ -154,6 +154,8 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
zkSyncEra = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC),
polygonZkEvm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC),
base = GetBlockAccessToken(rest = accessTokens.base?.jsonRPC),
blast = GetBlockAccessToken(jsonRpc = accessTokens.blast?.jsonRPC),
filecoin = GetBlockAccessToken(jsonRpc = accessTokens.filecoin?.jsonRPC),
)
}
}

View file

@ -76,6 +76,8 @@ data class GetBlockAccessTokens(
@Json(name = "polygon-zkevm") val polygonZkevm: GetBlockToken?,
@Json(name = "zksync") val zksync: GetBlockToken?,
@Json(name = "base") val base: GetBlockToken?,
@Json(name = "blast") val blast: GetBlockToken?,
@Json(name = "filecoin") val filecoin: GetBlockToken?,
)
@JsonClass(generateAdapter = true)

View file

@ -3,6 +3,10 @@ package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.config.ApiConfig
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.ProdApiConfigsManager
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.markets.TangemTechMarketsApi
@ -10,12 +14,15 @@ 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.tangemTech.TangemTechServiceApi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.utils.RequestHeader
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
import dagger.Module
import dagger.Provides
@ -32,6 +39,19 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
class NetworkModule {
@Provides
@Singleton
fun provideApiConfigManager(
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): ApiConfigsManager {
return if (BuildConfig.TESTER_MENU_ENABLED) {
DevApiConfigsManager(appPreferencesStore, dispatchers)
} else {
ProdApiConfigsManager()
}
}
@Provides
@Singleton
fun provideExpressApi(
@ -39,18 +59,15 @@ class NetworkModule {
@ApplicationContext context: Context,
expressAuthProvider: ExpressAuthProvider,
appVersionProvider: AppVersionProvider,
apiConfigsManager: ApiConfigsManager,
): TangemExpressApi {
val url = if (BuildConfig.ENVIRONMENT == "dev") {
STAGE_EXPRESS_BASE_URL
} else {
PROD_EXPRESS_BASE_URL
}
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.baseUrl(url)
.baseUrl(apiConfigsManager.getBaseUrl(id = ApiConfig.ID.Express))
.client(
OkHttpClient.Builder()
.addEnvironmentSwitcher(ApiConfig.ID.Express, apiConfigsManager)
.addHeaders(Express(expressAuthProvider))
.addHeaders(AppVersionPlatformHeaders(appVersionProvider))
.addLoggers(context)
@ -87,8 +104,15 @@ class NetworkModule {
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
appVersionProvider: AppVersionProvider,
apiConfigsManager: ApiConfigsManager,
): TangemTechApi {
return provideTangemTechApiInternal(moshi, context, appVersionProvider, PROD_V1_TANGEM_TECH_BASE_URL)
return provideTangemTechApiInternal(
moshi = moshi,
context = context,
appVersionProvider = appVersionProvider,
apiConfigsManager = apiConfigsManager,
baseUrl = apiConfigsManager.getBaseUrl(id = ApiConfig.ID.TangemTech),
)
}
@Provides
@ -97,8 +121,15 @@ class NetworkModule {
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
appVersionProvider: AppVersionProvider,
apiConfigsManager: ApiConfigsManager,
): TangemTechApiV2 {
return provideTangemTechApiInternal(moshi, context, appVersionProvider, PROD_V2_TANGEM_TECH_BASE_URL)
return provideTangemTechApiInternal(
moshi = moshi,
context = context,
appVersionProvider = appVersionProvider,
apiConfigsManager = apiConfigsManager,
baseUrl = PROD_V2_TANGEM_TECH_BASE_URL,
)
}
@Provides
@ -108,8 +139,15 @@ class NetworkModule {
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
appVersionProvider: AppVersionProvider,
apiConfigsManager: ApiConfigsManager,
): TangemTechApi {
return provideTangemTechApiInternal(moshi, context, appVersionProvider, DEV_V1_TANGEM_TECH_BASE_URL)
return provideTangemTechApiInternal(
moshi = moshi,
context = context,
appVersionProvider = appVersionProvider,
apiConfigsManager = apiConfigsManager,
baseUrl = DEV_V1_TANGEM_TECH_BASE_URL,
)
}
@Provides
@ -118,12 +156,14 @@ class NetworkModule {
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
appVersionProvider: AppVersionProvider,
apiConfigsManager: ApiConfigsManager,
): TangemTechServiceApi {
return provideTangemTechApiInternal(
moshi = moshi,
context = context,
appVersionProvider = appVersionProvider,
baseUrl = PROD_V1_TANGEM_TECH_BASE_URL,
apiConfigsManager = apiConfigsManager,
baseUrl = apiConfigsManager.getBaseUrl(id = ApiConfig.ID.TangemTech),
timeouts = Timeouts(
callTimeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS,
),
@ -138,11 +178,13 @@ class NetworkModule {
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
appVersionProvider: AppVersionProvider,
apiConfigsManager: ApiConfigsManager,
): TangemTechMarketsApi {
return provideTangemTechApiInternal(
moshi = moshi,
context = context,
appVersionProvider = appVersionProvider,
apiConfigsManager = apiConfigsManager,
baseUrl = DEV_V1_TANGEM_TECH_BASE_URL,
timeouts = Timeouts(
callTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS,
@ -157,11 +199,13 @@ class NetworkModule {
moshi: Moshi,
context: Context,
appVersionProvider: AppVersionProvider,
apiConfigsManager: ApiConfigsManager,
baseUrl: String,
timeouts: Timeouts = Timeouts(),
requestHeaders: List<RequestHeader> = listOf(CacheControlHeader, AppVersionPlatformHeaders(appVersionProvider)),
): T {
val client = OkHttpClient.Builder()
.addEnvironmentSwitcher(id = ApiConfig.ID.TangemTech, apiConfigsManager = apiConfigsManager)
.let { builder ->
var b = builder
if (timeouts.callTimeoutSeconds != null) {
@ -204,13 +248,9 @@ class NetworkModule {
private companion object {
const val STAKEKIT_BASE_URL = "https://api.stakek.it/v1/"
const val PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/"
const val STAGE_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]"
const val DEV_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]"
const val DEV_V1_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/"
const val PROD_V1_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/"
const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/"
const val TANGEM_TECH_SERVICE_TIMEOUT_SECONDS = 5L

View file

@ -95,6 +95,8 @@ object PreferencesKeys {
booleanPreferencesKey(name = "isTokenSwapPromoOkxShown")
}
val apiConfigsEnvironmentKey by lazy { stringPreferencesKey(name = "apiConfigsEnvironment") }
// region Permission
fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission")

View file

@ -125,7 +125,7 @@ suspend inline fun <reified V> AppPreferencesStore.storeObjectMap(
}
/** Get map with [String] key and value [V] by string [key], or empty if data is not found */
suspend inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences.Key<String>): Map<String, V> {
suspend inline fun <reified V> AppPreferencesStore.getObjectMapSync(key: Preferences.Key<String>): Map<String, V> {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
@ -135,6 +135,14 @@ suspend inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences
.orEmpty()
}
/** Get flow of map with [String] key and value [V] by string [key], or empty if data is not found */
inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences.Key<String>): Flow<Map<String, V>> {
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
return data.map { it[key]?.let(adapter::fromJson) ?: emptyMap() }
}
/** Get set of data [T] by string [key], or empty if data is not found */
suspend inline fun <reified T> AppPreferencesStore.getObjectSetSync(key: Preferences.Key<String>): Set<T> {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))

View file

@ -3,6 +3,9 @@ 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.config.ApiConfig
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import okhttp3.Interceptor
import okhttp3.OkHttpClient
@ -25,7 +28,7 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade
/**
* Extension for logging each [OkHttpClient] request
*
* @param level logging level. By default, only the request body.
* @param context context
*/
internal fun OkHttpClient.Builder.addLoggers(context: Context? = null): OkHttpClient.Builder {
return if (BuildConfig.LOG_ENABLED) {
@ -36,4 +39,26 @@ internal fun OkHttpClient.Builder.addLoggers(context: Context? = null): OkHttpCl
} else {
this
}
}
/**
* Add environment switcher
*
* @param id class of [ApiConfig]
* @param apiConfigsManager api configs manager
*/
internal fun OkHttpClient.Builder.addEnvironmentSwitcher(
id: ApiConfig.ID,
apiConfigsManager: ApiConfigsManager,
): OkHttpClient.Builder {
return if (BuildConfig.TESTER_MENU_ENABLED) {
addInterceptor(
interceptor = SwitchBaseUrlInterceptor(
id = id,
apiConfigsManager = apiConfigsManager,
),
)
} else {
this
}
}

View file

@ -0,0 +1,77 @@
package com.tangem.datasource.api.common.config.managers
import com.google.common.truth.Truth
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfig.Companion.DEBUG_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
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 org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
/**
[REDACTED_AUTHOR]
*/
@RunWith(Parameterized::class)
internal class ProdApiConfigsManagerTest(private val model: Model) {
private val manager = ProdApiConfigsManager()
@Test
fun test_getBaseUrl() {
val actual = manager.getBaseUrl(id = model.id)
Truth.assertThat(actual).isEqualTo(model.expected)
}
data class Model(val id: ApiConfig.ID, val expected: String)
private companion object {
@JvmStatic
@Parameterized.Parameters
fun data(): Collection<Model> = ApiConfig.values().map {
when (it) {
is Express -> createExpressModel()
is TangemTech -> createTangemTechModel()
}
}
private fun createExpressModel(): Model {
return Model(
id = ApiConfig.ID.Express,
expected = when (BuildConfig.BUILD_TYPE) {
DEBUG_BUILD_TYPE -> "[REDACTED_ENV_URL]"
INTERNAL_BUILD_TYPE,
MOCKED_BUILD_TYPE,
-> "[REDACTED_ENV_URL]"
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> "https://express.tangem.com/v1/"
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
},
)
}
private fun createTangemTechModel(): Model {
return Model(
id = ApiConfig.ID.TangemTech,
expected = when (BuildConfig.BUILD_TYPE) {
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
-> "https://devapi.tangem-tech.com/v1/"
MOCKED_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> "https://api.tangem-tech.com/v1/"
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
},
)
}
}
}

View file

@ -3,10 +3,6 @@
"name": "NEW_CARD_SCANNING_ENABLED",
"version": "undefined"
},
{
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
"version": "5.10.0"
},
{
"name": "LOCAL_USER_LOGS_ENABLED",
"version": "5.13.0"
@ -38,5 +34,13 @@
{
"name": "MARKETS_ENABLED",
"version": "undefined"
},
{
"name": "HOME_SCREEN_CALLBACKS_REFACTORING_ENABLED",
"version": "5.14.0"
},
{
"name": "NEW_MANAGE_TOKENS",
"version": "undefined"
}
]

View file

@ -114,6 +114,12 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
loadMoreActionJob?.cancel()
reloadActionJob?.cancel()
stopAllUpdates()
state.value = BatchListState(
data = emptyList(),
status = PaginationStatus.InitialLoading,
)
reloadActionJob = scope.launchFetch {
reloadTask(action)
}
@ -221,11 +227,6 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
}
private suspend fun reloadTask(action: BatchAction.Reload<TRequestParams>) {
state.value = BatchListState(
data = emptyList(),
status = PaginationStatus.InitialLoading,
)
val res = runCatching {
batchFetcher.fetchFirst(action.requestParams)
}.getOrElse {

View file

@ -48,7 +48,7 @@
<string name="common_biometrics">生物</string>
<string name="common_buy">購買</string>
<string name="common_camera_denied_alert_message">您尚未授予相機訪問權限,請更改您的隱私設置</string>
<string name="common_cancel"></string>
<string name="common_cancel"></string>
<string name="common_close">關閉</string>
<string name="common_continue">繼續</string>
<string name="common_copy">複製</string>

View file

@ -2,14 +2,14 @@ package com.tangem.core.ui
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Stable
import com.tangem.core.ui.haptic.HapticManager
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.EventMessageHandler
import com.tangem.core.ui.theme.AppThemeModeHolder
@Stable
interface UiDependencies {
val hapticManager: HapticManager
val vibratorHapticManager: VibratorHapticManager
val appThemeModeHolder: AppThemeModeHolder

View file

@ -10,6 +10,7 @@ import androidx.compose.material.ButtonColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.*
@ -26,6 +27,7 @@ fun TextButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
colors: ButtonColors = TangemButtonsDefaults.defaultTextButtonColors,
textStyle: TextStyle = TangemTheme.typography.button,
enabled: Boolean = true,
) {
TangemButton(
@ -37,6 +39,7 @@ fun TextButton(
showProgress = false,
colors = colors,
size = TangemButtonSize.Text,
textStyle = textStyle,
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.core.ui.components
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.ime
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.platform.LocalDensity
@ -14,11 +15,15 @@ sealed interface Keyboard {
data class Opened(override val height: Dp) : Keyboard
object Closed : Keyboard {
data object Closed : Keyboard {
override val height: Dp = 0.dp
}
}
val Keyboard.isOpened: Boolean
@Stable
get() = this is Keyboard.Opened
/**
* Allows to subscribe to a soft keyboard to detect when it's open/closed
*/

View file

@ -9,14 +9,23 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.PrimarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.*
import com.valentinilk.shimmer.*
@ -45,6 +54,70 @@ fun CircleShimmer(modifier: Modifier = Modifier) {
)
}
/**
* Shimmer for text
* Height will be set automatically
*
* @param textSizeHeight if true, height will be set to font size height.
*/
@Composable
fun TextShimmer(
style: TextStyle,
modifier: Modifier = Modifier,
text: String = "A",
radius: Dp = TangemTheme.dimens.radius3,
textSizeHeight: Boolean = false,
) {
if (textSizeHeight) {
val lineHeight = with(LocalDensity.current) { style.lineHeight.toDp() }
Box(
modifier = Modifier.requiredHeight(lineHeight),
contentAlignment = Alignment.CenterStart,
) {
Text(
modifier = modifier
.clip(RoundedCornerShape(size = radius))
.shimmer(LocalTangemShimmer.current),
text = text,
style = style.copy(lineHeight = style.fontSize),
maxLines = 1,
)
}
} else {
Text(
modifier = modifier
.clip(RoundedCornerShape(size = radius))
.shimmer(LocalTangemShimmer.current),
text = text,
style = style,
maxLines = 1,
)
}
}
/**
* Shimmer for SmallButton
* Height and min width will be set automatically
*/
@Composable
fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false) {
PrimarySmallButton(
config = SmallButtonConfig(
text = stringReference("B"),
onClick = {},
icon = if (withIcon) {
TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24)
} else {
TangemButtonIconPosition.None
},
),
modifier = modifier
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius16))
.shimmer(LocalTangemShimmer.current),
)
}
internal val TangemShimmer: Shimmer
@Composable
get() = rememberShimmer(
@ -106,6 +179,17 @@ private fun ShimmersPreview() {
.height(TangemTheme.dimens.size24),
)
CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42))
TextShimmer(
style = TangemTheme.typography.body1,
modifier = Modifier.fillMaxWidth(fraction = 0.4f),
)
TextShimmer(
style = TangemTheme.typography.body1,
textSizeHeight = true,
modifier = Modifier.fillMaxWidth(fraction = 0.4f),
)
SmallButtonShimmer(withIcon = true)
SmallButtonShimmer(withIcon = false)
}
}
}

View file

@ -97,7 +97,6 @@ private fun Preview_Grid() {
},
content = {
GridItems(
itemPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing4),
items = persistentListOf(
stringReference("Fist item"),
stringReference("Second item"),
@ -105,7 +104,6 @@ private fun Preview_Grid() {
itemContent = {
PreviewItem(text = it)
},
horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
)
},
)

View file

@ -44,7 +44,6 @@ inline fun <T : Any> InformationBlockContentScope.GridItems(
items: ImmutableList<T>,
itemContent: @Composable BoxScope.(T) -> Unit,
modifier: Modifier = Modifier,
itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0),
verticalAlignment: Alignment.Vertical = Alignment.Top,
horizontalArragement: Arrangement.Horizontal = Arrangement.Start,
) {
@ -59,7 +58,6 @@ inline fun <T : Any> InformationBlockContentScope.GridItems(
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
) {
rowItems.fastForEach { row ->
@ -70,10 +68,7 @@ inline fun <T : Any> InformationBlockContentScope.GridItems(
) {
row.fastForEach { item ->
Box(
modifier = Modifier
.padding(itemPadding)
.weight(1f),
contentAlignment = Alignment.Center,
modifier = Modifier.weight(1f),
) {
itemContent(item)
}

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
@ -29,6 +30,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
titleAction: TopAppBarButtonUM? = null,
containerColor: Color = TangemTheme.colors.background.primary,
addBottomInsets: Boolean = true,
skipPartiallyExpanded: Boolean = true,
crossinline content: @Composable ColumnScope.(T) -> Unit,
) {
TangemBottomSheet(
@ -36,6 +38,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
containerColor = containerColor,
addBottomInsets = addBottomInsets,
title = { TangemBottomSheetTitle(title = titleText, endButton = titleAction) },
skipPartiallyExpanded = skipPartiallyExpanded,
content = content,
)
}
@ -48,6 +51,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color = TangemTheme.colors.background.primary,
addBottomInsets: Boolean = true,
skipPartiallyExpanded: Boolean = true,
crossinline title: @Composable BoxScope.(T) -> Unit = {},
crossinline content: @Composable ColumnScope.(T) -> Unit,
) {
@ -60,6 +64,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
addBottomInsets = addBottomInsets,
title = title,
content = content,
skipPartiallyExpanded = skipPartiallyExpanded,
)
} else {
DefaultBottomSheet<T>(
@ -68,6 +73,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
addBottomInsets = addBottomInsets,
title = title,
content = content,
skipPartiallyExpanded = skipPartiallyExpanded,
)
}
}
@ -78,11 +84,12 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color,
addBottomInsets: Boolean,
skipPartiallyExpanded: Boolean = true,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
var isVisible by remember { mutableStateOf(value = config.isShow) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
if (isVisible && config.content is T) {
BasicBottomSheet<T>(
@ -110,13 +117,15 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color,
addBottomInsets: Boolean,
skipPartiallyExpanded: Boolean = true,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
BasicBottomSheet<T>(
modifier = Modifier.width(360.dp),
config = config,
sheetState = SheetState(
skipPartiallyExpanded = true,
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
density = LocalDensity.current,
),
@ -137,6 +146,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
addBottomInsets: Boolean,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
modifier: Modifier = Modifier,
) {
val model = config.content as? T ?: return
@ -145,7 +155,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
ModalBottomSheet(
// FIXME temporary solution to fix height of the bottom sheet
modifier = Modifier.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight),
modifier = modifier.heightIn(max = LocalWindowSize.current.height - statusBarHeight),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,

View file

@ -11,7 +11,7 @@ sealed interface TangemButtonIconPosition {
data class End(@DrawableRes override val iconResId: Int) : TangemButtonIconPosition
object None : TangemButtonIconPosition {
data object None : TangemButtonIconPosition {
@DrawableRes
override val iconResId: Int? = null
}

View file

@ -1,12 +1,11 @@
package com.tangem.core.ui.components.fields
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@ -40,6 +39,7 @@ fun SimpleTextField(
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
placeholderColor: Color = TangemTheme.colors.text.disabled,
readOnly: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
isValuePasted: Boolean = false,
onValuePastedTriggerDismiss: () -> Unit = {},
decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null,
@ -54,10 +54,6 @@ fun SimpleTextField(
)
}
val focusRequester = remember { FocusRequester.Default }
val customTextSelectionColors = TextSelectionColors(
handleColor = TangemTheme.colors.text.accent,
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
)
val textFieldValue = textFieldValueState.copy(text = value)
var lastTextValue by remember(proxyValue, isValuePasted) {
textFieldValueState = textFieldValueState.copy(
@ -85,37 +81,36 @@ fun SimpleTextField(
}
}
CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) {
BasicTextField(
value = textFieldValue,
onValueChange = { newTextFieldValueState ->
textFieldValueState = newTextFieldValueState
BasicTextField(
value = textFieldValue,
onValueChange = { newTextFieldValueState ->
textFieldValueState = newTextFieldValueState
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
lastTextValue = newTextFieldValueState.text
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
lastTextValue = newTextFieldValueState.text
if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text)
},
textStyle = textStyle.copy(color = color),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
readOnly = readOnly,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
decorationBox = decorationBox ?: { textValue ->
SimpleTextPlaceholder(
placeholder = placeholder,
value = value,
textStyle = textStyle,
textValue = textValue,
color = placeholderColor,
)
},
modifier = modifier
.focusRequester(focusRequester),
)
}
if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text)
},
textStyle = textStyle.copy(color = color),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
readOnly = readOnly,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
decorationBox = decorationBox ?: { textValue ->
SimpleTextPlaceholder(
placeholder = placeholder,
value = value,
textStyle = textStyle,
textValue = textValue,
color = placeholderColor,
)
},
modifier = modifier
.focusRequester(focusRequester),
)
}
@Composable

View file

@ -72,7 +72,7 @@ fun InputRowEnter(
Column(modifier = Modifier.weight(1f)) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
SimpleTextField(

View file

@ -72,7 +72,7 @@ fun InputRowEnterAmount(
Column(modifier = Modifier.weight(1f)) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
AmountTextField(

View file

@ -62,7 +62,7 @@ fun InputRowEnterInfo(
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
Row {

View file

@ -70,7 +70,7 @@ fun InputRowEnterInfoAmount(
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
Row {

View file

@ -70,7 +70,7 @@ fun InputRowImage(
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
Row(

View file

@ -15,7 +15,7 @@ import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun InputRowImageBase(
subtitle: TextReference,
caption: TextReference,
caption: TextReference?,
imageUrl: String,
modifier: Modifier = Modifier,
subtitleColor: Color = TangemTheme.colors.text.primary1,
@ -40,12 +40,14 @@ internal fun InputRowImageBase(
style = TangemTheme.typography.subtitle2,
color = subtitleColor,
)
Text(
text = caption.resolveAnnotatedReference(),
style = TangemTheme.typography.caption2,
color = captionColor,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
)
if (caption != null) {
Text(
text = caption.resolveAnnotatedReference(),
style = TangemTheme.typography.caption2,
color = captionColor,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
)
}
}
extraContent()
}

View file

@ -75,7 +75,7 @@ fun InputRowRecipient(
AnimatedContent(targetState = titleText, label = "Title Change") {
Text(
text = it.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = color,
)
}

View file

@ -50,7 +50,7 @@ fun InputRowRecipientDefault(
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
Row(

View file

@ -4,6 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -15,24 +16,57 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
private const val ROUNDED_LIST_WITH_DIVIDERS_HEADER_KEY = "ROUNDED_LIST_WITH_DIVIDERS_HEADER_KEY"
private const val ROUNDED_LIST_WITH_DIVIDERS_FOOTER_KEY = "ROUNDED_LIST_WITH_DIVIDERS_FOOTER_KEY"
@Composable
fun RoundedListWithDividers(rows: List<RoundedListWithDividersItemData>, modifier: Modifier = Modifier) {
fun RoundedListWithDividers(
rows: ImmutableList<RoundedListWithDividersItemData>,
modifier: Modifier = Modifier,
headerContent: (@Composable () -> Unit)? = null,
footerContent: (@Composable () -> Unit)? = null,
) {
LazyColumn(modifier = modifier) {
itemsIndexed(
items = rows,
key = { _, item -> item.id },
) { index, row ->
InitialInfoContentRow(
startText = row.startText.resolveReference(),
endText = row.endText.resolveReference(),
cornersToRound = getCornersToRound(index, rows.size),
iconClick = row.iconClick,
)
if (index < rows.lastIndex) {
RoundedListDivider()
}
this.roundedListWithDividersItems(
rows = rows,
headerContent = headerContent,
footerContent = footerContent,
)
}
}
fun LazyListScope.roundedListWithDividersItems(
rows: ImmutableList<RoundedListWithDividersItemData>,
headerContent: (@Composable () -> Unit)? = null,
footerContent: (@Composable () -> Unit)? = null,
) {
if (headerContent != null) {
item(key = ROUNDED_LIST_WITH_DIVIDERS_HEADER_KEY) {
headerContent()
}
}
itemsIndexed(
items = rows,
key = { _, item -> item.id },
) { index, row ->
InitialInfoContentRow(
startText = row.startText.resolveReference(),
endText = row.endText.resolveReference(),
cornersToRound = getCornersToRound(index, rows.size),
iconClick = row.iconClick,
)
if (index < rows.lastIndex) {
RoundedListDivider()
}
}
if (footerContent != null) {
item(key = ROUNDED_LIST_WITH_DIVIDERS_FOOTER_KEY) {
footerContent()
}
}
}

View file

@ -13,6 +13,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -28,7 +29,7 @@ fun TooltipText(
text: TextReference,
onInfoClick: () -> Unit,
modifier: Modifier = Modifier,
useSmallerText: Boolean = false,
textStyle: TextStyle = TangemTheme.typography.caption2,
) {
val interactionSource = remember { MutableInteractionSource() }
@ -45,18 +46,16 @@ fun TooltipText(
Text(
modifier = Modifier.weight(1f, fill = false),
text = text.resolveReference(),
style = if (useSmallerText) {
TangemTheme.typography.caption2
} else {
TangemTheme.typography.subtitle2
},
style = textStyle,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
IconButton(
modifier = Modifier.requiredSize(TangemTheme.dimens.size24),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing4)
.requiredSize(TangemTheme.dimens.size16),
interactionSource = interactionSource,
onClick = onInfoClick,
) {

View file

@ -3,7 +3,7 @@ package com.tangem.core.ui.extensions
import androidx.annotation.DrawableRes
import com.tangem.core.ui.R
@Suppress("ComplexMethod")
@Suppress("ComplexMethod", "LongMethod")
@DrawableRes
fun getActiveIconRes(blockchainId: String): Int {
return when (blockchainId) {
@ -70,11 +70,13 @@ fun getActiveIconRes(blockchainId: String): Int {
"joystream" -> R.drawable.img_joystream_22
"koinos", "koinos/test" -> R.drawable.img_koinos_22
"bittensor" -> R.drawable.img_bittensor_22
"blast", "blast/test" -> R.drawable.img_blast_22
"filecoin" -> R.drawable.img_filecoin_22
else -> R.drawable.ic_alert_24
}
}
@Suppress("ComplexMethod")
@Suppress("ComplexMethod", "LongMethod")
@DrawableRes
fun getActiveIconResByNetworkId(networkId: String): Int {
return when (networkId) {
@ -141,6 +143,8 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
"joystream" -> R.drawable.img_joystream_22
"koinos", "koinos/test" -> R.drawable.img_koinos_22
"bittensor" -> R.drawable.img_bittensor_22
"blast", "blast/test" -> R.drawable.img_blast_22
"filecoin" -> R.drawable.img_filecoin_22
else -> R.drawable.ic_alert_24
}
}
@ -209,11 +213,13 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"joystream" -> R.drawable.img_joystream_22
"koinos", "koinos/test" -> R.drawable.img_koinos_22
"bittensor" -> R.drawable.img_bittensor_22
"blast", "blast/test" -> R.drawable.img_blast_22
"filecoin" -> R.drawable.img_filecoin_22
else -> R.drawable.ic_alert_24
}
}
@Suppress("ComplexMethod")
@Suppress("ComplexMethod", "LongMethod")
@DrawableRes
fun getGreyedOutIconRes(blockchainId: String): Int {
return when (blockchainId) {
@ -280,11 +286,13 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"joystream" -> R.drawable.ic_joystream_22
"koinos", "koinos/test" -> R.drawable.ic_koinos_22
"bittensor" -> R.drawable.ic_bittensor_22
"blast", "blast/test" -> R.drawable.ic_blast_22
"filecoin" -> R.drawable.ic_filecoin_22
else -> R.drawable.ic_alert_24
}
}
@Suppress("ComplexMethod")
@Suppress("ComplexMethod", "LongMethod")
@DrawableRes
fun getGreyedOutIconResByNetworkId(networkId: String): Int {
return when (networkId) {
@ -351,6 +359,8 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
"joystream" -> R.drawable.ic_joystream_22
"koinos", "koinos/test" -> R.drawable.ic_koinos_22
"bittensor" -> R.drawable.ic_bittensor_22
"blast", "blast/test" -> R.drawable.ic_blast_22
"filecoin" -> R.drawable.ic_filecoin_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.core.ui.haptic
import android.view.View
import androidx.core.view.ViewCompat
internal class DefaultHapticManager(
private val view: View,
private val vibratorHapticManager: VibratorHapticManager?,
) : HapticManager {
override fun perform(effect: TangemHapticEffect) {
when (effect) {
is TangemHapticEffect.View -> {
effect.androidHapticFeedbackCode?.let {
ViewCompat.performHapticFeedback(view, it)
}
}
is TangemHapticEffect.OneTime -> {
if (vibratorHapticManager != null) {
vibratorHapticManager.performOneTime(effect)
} else {
when (effect) {
TangemHapticEffect.OneTime.Tick -> perform(TangemHapticEffect.View.SegmentTick)
TangemHapticEffect.OneTime.Click -> perform(TangemHapticEffect.View.ContextClick)
TangemHapticEffect.OneTime.DoubleClick -> perform(TangemHapticEffect.View.ContextClick)
TangemHapticEffect.OneTime.HeavyClick -> perform(TangemHapticEffect.View.LongPress)
}
}
}
}
}
}

View file

@ -2,12 +2,13 @@ package com.tangem.core.ui.haptic
import androidx.compose.runtime.Stable
/**
* Haptic feedback.
* @see [TangemHapticEffect.OneTime] for one-time effects.
* @see [TangemHapticEffect.View] for view effects.
*/
@Stable
interface HapticManager {
fun vibrateShort()
fun vibrateMeduim()
fun vibrateLong()
fun perform(effect: TangemHapticEffect)
}

View file

@ -1,30 +0,0 @@
package com.tangem.core.ui.haptic
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
@Suppress("FunctionName")
fun MockHapticManager(mockHapticFeedback: HapticFeedback? = null): HapticManager =
if (mockHapticFeedback == null) MockHapticManager else MockHapticManagerImpl(mockHapticFeedback)
val MockHapticManager: HapticManager = MockHapticManagerImpl()
private class MockHapticManagerImpl(
private val mockHapticFeedback: HapticFeedback? = null,
) : HapticManager {
override fun vibrateShort() {
mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
/** Intentionally do nothing */
}
override fun vibrateMeduim() {
mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
/** Intentionally do nothing */
}
override fun vibrateLong() {
mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.LongPress)
/** Intentionally do nothing */
}
}

View file

@ -0,0 +1,125 @@
package com.tangem.core.ui.haptic
import android.os.Build
import android.os.VibrationEffect
import androidx.annotation.RequiresApi
sealed interface TangemHapticEffect {
/**
* For cases when view could not be visible on screen (ex. Activity is not in foreground)
* or there is no view context (ex. background service, Model, ViewModel)
*/
enum class OneTime : TangemHapticEffect {
Click,
DoubleClick,
HeavyClick,
Tick,
;
val code: Int
@RequiresApi(Build.VERSION_CODES.Q)
get() = when (this) {
Click -> VibrationEffect.EFFECT_CLICK
DoubleClick -> VibrationEffect.EFFECT_DOUBLE_CLICK
HeavyClick -> VibrationEffect.EFFECT_HEAVY_CLICK
Tick -> VibrationEffect.EFFECT_TICK
}
}
/**
* Preferred way to provide haptic feedback for UI components
* @see [androidx.core.view.HapticFeedbackConstantsCompat]
*/
@Suppress("MagicNumber")
enum class View(internal val androidHapticFeedbackCode: Int? = null) : TangemHapticEffect {
/**
* The user has performed a long press on an object that is resulting in an action being
* performed
*/
LongPress(0),
/**
* The user has pressed on a virtual on-screen key
*/
VirtualKey(1),
/**
* The user has pressed either an hour or minute tick of a Clock
*/
ClockTick(4),
/**
* The user has performed a context click on an object
*/
ContextClick(6),
/**
* The user has pressed a virtual or software keyboard key
*/
KeyboardPress(3),
/**
* The user has released a virtual keyboard key
*/
KeyboardRelease(7),
/**
* The user has released a virtual key
*/
VirtualKeyRelease(8),
/**
* The user has performed a selection/insertion handle move on text field
*/
TextHandleMove(9),
/**
* The user has started a gesture (e.g. on the soft keyboard)
*/
GestureStart(12),
/**
* The user has finished a gesture (e.g. on the soft keyboard)
*/
GestureEnd(13),
/**
* A haptic effect to signal the confirmation or successful completion of a user interaction
*/
Confirm(16),
/**
* A haptic effect to signal the rejection or failure of a user interaction
*/
Reject(17),
/**
* The user has toggled a switch or button into the on position
*/
ToggleOn(21),
/**
* The user has toggled a switch or button into the off position
*/
ToggleOff(22),
/**
* The user is executing a swipe/drag-style gesture, such as pull-to-refresh, where the
* gesture action is eligible at a certain threshold of movement, and can be cancelled by
* moving back past the threshold. This constant indicates that the user's motion has just
* passed the threshold for the action to be activated on release
*/
GestureThresholdActivate(23),
/**
* The user is executing a swipe/drag-style gesture, such as pull-to-refresh, where the
* gesture action is eligible at a certain threshold of movement, and can be cancelled by
* moving back past the threshold. This constant indicates that the user's motion has just
* re-crossed back "under" the threshold for the action to be activated, meaning the gesture is
* currently in a cancelled state
*/
GestureThresholdDeactivate(24),
/**
* The user has started a drag-and-drop gesture. The drag target has just been "picked up"
*/
DragStart(25),
/**
* The user is switching between a series of potential choices, for example items in a list
* or discrete points on a slider
*/
SegmentTick(26),
/**
* The user is switching between a series of many potential choices, for example minutes on a
* clock face, or individual percentages. This constant is expected to be very soft, so as
* not to be uncomfortable when performed a lot in quick succession. If the device cant make
* a suitably soft vibration, then it may not make any vibration
*/
SegmentFrequentTick(27),
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.core.ui.haptic
import androidx.compose.runtime.Stable
/**
* Haptic feedback
* For cases when view could not be visible on screen (ex. Activity is not in foreground)
* or there is no view context (ex. background service, Model, ViewModel)
* @see [HapticManager] for view effects.
*/
@Stable
interface VibratorHapticManager {
fun performOneTime(effect: TangemHapticEffect.OneTime)
}

View file

@ -0,0 +1,26 @@
package com.tangem.core.ui.res
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.runtime.*
@Immutable
object TangemAnimations {
val transitionSpecs = TransitionSpecs
@Composable
@NonRestartableComposable
fun horizontalIndicatorAsState(targetFraction: Float): State<Float> {
return animateFloatAsState(
targetValue = targetFraction,
animationSpec = tween(durationMillis = 300),
label = "Indicator fraction",
)
}
@Immutable
object TransitionSpecs {
// TODO add more transition specs
}
}

View file

@ -8,10 +8,12 @@ import androidx.compose.material.ProvideTextStyle
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.components.TangemShimmer
import com.tangem.core.ui.haptic.DefaultHapticManager
import com.tangem.core.ui.haptic.HapticManager
import com.tangem.core.ui.haptic.MockHapticManager
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.windowsize.WindowSize
import com.valentinilk.shimmer.Shimmer
@ -21,7 +23,7 @@ fun TangemTheme(
windowSize: WindowSize,
typography: TangemTypography = TangemTheme.typography,
dimens: TangemDimens = TangemTheme.dimens,
hapticManager: HapticManager = MockHapticManager,
vibratorHapticManager: VibratorHapticManager? = null,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
content: @Composable () -> Unit,
) {
@ -40,6 +42,12 @@ fun TangemTheme(
)
}
val view = LocalView.current
val hapticManager = remember(view) {
DefaultHapticManager(view = view, vibratorHapticManager = vibratorHapticManager)
}
MaterialTheme(
colors = materialThemeColors(colors = themeColors, isDark = isDark),
) {
@ -52,10 +60,11 @@ fun TangemTheme(
LocalHapticManager provides hapticManager,
LocalSnackbarHostState provides snackbarHostState,
LocalWindowSize provides windowSize,
LocalTextSelectionColors provides TangemTextSelectionColors,
) {
CompositionLocalProvider(
LocalTangemShimmer provides TangemShimmer,
LocalMainBottomSheetColor provides remember { mutableStateOf(Color.Unspecified) },
LocalTextSelectionColors provides TangemTextSelectionColors,
) {
ProvideTextStyle(
value = TangemTheme.typography.body1,
@ -208,11 +217,13 @@ private fun darkThemeColors(): TangemColors {
)
}
@Stable
private val TangemTextSelectionColors = TextSelectionColors(
handleColor = TangemColorPalette.Azure,
backgroundColor = TangemColorPalette.Azure.copy(alpha = 0.4f),
)
private val TangemTextSelectionColors: TextSelectionColors
@Composable
@ReadOnlyComposable
get() = TextSelectionColors(
handleColor = TangemTheme.colors.text.accent,
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
)
private val LocalTangemColors = staticCompositionLocalOf<TangemColors> {
error("No TangemColors provided")
@ -246,4 +257,8 @@ val LocalWindowSize = staticCompositionLocalOf<WindowSize> {
val LocalTangemShimmer = staticCompositionLocalOf<Shimmer> {
error("No TangemShimmer provided")
}
val LocalMainBottomSheetColor = staticCompositionLocalOf<MutableState<Color>> {
error("No MainBottomSheetColor provided")
}

View file

@ -6,8 +6,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.platform.LocalHapticFeedback
import com.tangem.core.ui.haptic.MockHapticManager
import com.tangem.core.ui.windowsize.rememberWindowSizePreview
@Composable
@ -29,7 +27,6 @@ fun TangemThemePreview(
typography = typography,
dimens = dimens,
windowSize = rememberWindowSizePreview(maxWidth, maxHeight),
hapticManager = MockHapticManager(LocalHapticFeedback.current),
content = content,
)
}

View file

@ -61,7 +61,7 @@ internal fun ComposeScreen.createComposeView(context: Context, activity: Activit
TangemTheme(
isDark = shouldUseDarkTheme(appThemeMode),
windowSize = windowSize,
hapticManager = uiDependencies.hapticManager,
vibratorHapticManager = uiDependencies.vibratorHapticManager,
snackbarHostState = uiDependencies.globalSnackbarHostState,
) {
ScreenContent(modifier = screenModifier)

View file

@ -117,6 +117,7 @@ object BigDecimalFormatter {
fiatAmount: BigDecimal?,
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
decimals: Int = FIAT_MARKET_DEFAULT_DIGITS,
locale: Locale = Locale.getDefault(),
): String {
if (fiatAmount == null) return EMPTY_BALANCE_SIGN
@ -124,8 +125,8 @@ object BigDecimalFormatter {
val formatterCurrency = getCurrency(fiatCurrencyCode)
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
currency = formatterCurrency
maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
maximumFractionDigits = decimals
minimumFractionDigits = decimals
roundingMode = RoundingMode.HALF_UP
}
@ -250,40 +251,27 @@ object BigDecimalFormatter {
/**
* "123456.6" -> "$123.457K"
* "12345.6" -> "$123.046K"
* Negative amount is not supported
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
* @param scale the number of digits to the right of the decimal point
*/
@Suppress("MagicNumber")
fun formatCompactAmount(
amount: BigDecimal,
fun formatCompactFiatAmount(
amount: BigDecimal?,
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
threeDigitsMethod: Boolean = false,
scale: Int = 0,
locale: Locale = Locale.getDefault(),
): String {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return BigDecimalFormatterCompat.formatCompactAmountNoLocaleContext(
amount = amount,
fiatCurrencyCode = fiatCurrencyCode,
fiatCurrencySymbol = fiatCurrencySymbol,
locale = locale,
)
}
if (amount == null) return EMPTY_BALANCE_SIGN
val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP)
val digitsCount = scaledAmount.longValueExact().toString().count()
val digitsToFormat = 6 - when (digitsCount % 3) {
0 -> 0
1 -> 2
else -> 1
}
val formatter = CompactDecimalFormat.getInstance(
locale,
CompactDecimalFormat.CompactStyle.SHORT,
).apply {
minimumSignificantDigits = 4
maximumSignificantDigits = digitsToFormat
}
val rawAmount = formatter.format(amount.setScale(0, RoundingMode.HALF_UP))
val rawAmount = formatCompactAmount(
amount = amount,
locale = locale,
threeDigitsMethod = threeDigitsMethod,
scale = scale,
)
return addCurrencySymbolToStringAmount(
amount = rawAmount,
@ -293,5 +281,53 @@ object BigDecimalFormatter {
)
}
/**
* "123456.6" -> "123.457K"
* "12345.6" -> "123.046K"
* Negative amount is not supported
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
* @param scale the number of digits to the right of the decimal point
*/
@Suppress("MagicNumber")
fun formatCompactAmount(
amount: BigDecimal,
locale: Locale = Locale.getDefault(),
threeDigitsMethod: Boolean = false,
scale: Int = 0,
): String {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return BigDecimalFormatterCompat.formatCompactAmountNoLocaleContext(amount = amount)
}
if (threeDigitsMethod) {
val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP)
val digitsCount = scaledAmount.longValueExact().toString().count()
val digitsToFormat = 6 - when (digitsCount % 3) {
0 -> 0
1 -> 2
else -> 1
}
val formatter = CompactDecimalFormat.getInstance(
locale,
CompactDecimalFormat.CompactStyle.SHORT,
).apply {
minimumSignificantDigits = 4
maximumSignificantDigits = digitsToFormat
}
return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP))
} else {
val value = amount.setScale(scale, RoundingMode.HALF_UP)
val formatter = CompactDecimalFormat.getInstance(
locale,
CompactDecimalFormat.CompactStyle.SHORT,
)
return formatter.format(value)
}
}
private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
}

View file

@ -7,16 +7,32 @@ import java.util.Locale
internal object BigDecimalFormatterCompat {
/**
* Formats value as [BigDecimalFormatter.formatCompactAmount] does using only "T","B","M","K" suffixes
* Formats value as [BigDecimalFormatter.formatCompactFiatAmount] does using only "T","B","M","K" suffixes
* Used for < API24 compatibility
*/
@Suppress("MagicNumber", "UnnecessaryParentheses")
fun formatCompactAmountNoLocaleContext(
fun formatCompactFiatAmountNoLocaleContext(
amount: BigDecimal,
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
locale: Locale = Locale.getDefault(),
): String {
val formatted = formatCompactAmountNoLocaleContext(amount)
return BigDecimalFormatter.addCurrencySymbolToStringAmount(
amount = formatted,
fiatCurrencyCode = fiatCurrencyCode,
fiatCurrencySymbol = fiatCurrencySymbol,
locale = locale,
)
}
/**
* Formats value as [BigDecimalFormatter.formatCompactAmount] does using only "T","B","M","K" suffixes
* Used for < API24 compatibility
*/
@Suppress("MagicNumber", "UnnecessaryParentheses")
fun formatCompactAmountNoLocaleContext(amount: BigDecimal): String {
val value = amount.setScale(0, RoundingMode.HALF_UP).longValueExact()
val formatted = when {
@ -42,11 +58,6 @@ internal object BigDecimalFormatterCompat {
else -> return value.toString()
}
return BigDecimalFormatter.addCurrencySymbolToStringAmount(
amount = formatted,
fiatCurrencyCode = fiatCurrencyCode,
fiatCurrencySymbol = fiatCurrencySymbol,
locale = locale,
)
return formatted
}
}

View file

@ -61,7 +61,14 @@ object DateTimeFormatters {
*/
val dateMMMMd: DateTimeFormatter by lazy {
DateTimeFormatterBuilder()
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d"))
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "dd MMM"))
.toFormatter()
.withLocale(Locale.getDefault())
}
val dateYYYY: DateTimeFormatter by lazy {
DateTimeFormatterBuilder()
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyy"))
.toFormatter()
.withLocale(Locale.getDefault())
}

View file

@ -0,0 +1,55 @@
package com.tangem.core.ui.utils
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.coroutines.delay
/**
* A container that shows a shimmer effect on top of the actual content.
* The shimmer effect is toggled every 2 seconds.
* Used for previewing components with shimmer effect and comparing their sizes with the actual content.
*
* If height is changing during the preview, it means that the actual content is not aligned with the shimmer effect.
*
* @param actualContent The actual content to be displayed.
* @param shimmerContent The shimmer effect to be displayed.
*/
@Composable
fun PreviewShimmerContainer(actualContent: @Composable () -> Unit, shimmerContent: @Composable () -> Unit) {
Column {
var height by remember { mutableIntStateOf(0) }
Row {
Text("height = $height")
}
SpacerH4()
var shimmerVisible by remember { mutableStateOf(true) }
TangemThemePreview {
Box(
Modifier.onGloballyPositioned {
height = it.size.height
},
) {
actualContent()
if (shimmerVisible) {
shimmerContent()
}
}
}
LaunchedEffect(Unit) {
while (true) {
delay(timeMillis = 2000)
shimmerVisible = !shimmerVisible
}
}
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.core.ui.utils
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
fun Modifier.disableNestedScroll(): Modifier = nestedScroll(DisableParentConnection)
private object DisableParentConnection : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return available.copy(x = 0f)
}
}

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:fillColor="#000000"
android:pathData="M15.257,11.048L17.71,9.826L18.555,7.231L16.864,6H5.604L3,7.934H16.236L15.533,10.111H10.225L9.714,11.701H15.022L13.532,16.281L16.019,15.05L16.906,12.304L15.24,11.082L15.257,11.048Z" />
<path
android:fillColor="#000000"
android:pathData="M6.742,14.313L8.275,9.541L6.575,8.269L4.021,16.281H13.532L14.168,14.313H6.742Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M12.045,9.68L11.715,11.44L14.85,11.88L14.63,12.705L11.55,12.265C11.33,12.98 11.22,13.75 10.945,14.41C10.67,15.18 10.395,15.95 10.065,16.665C9.625,17.6 8.855,18.26 7.81,18.425C7.205,18.535 6.545,18.48 6.05,18.095C5.885,17.985 5.72,17.765 5.72,17.6C5.72,17.38 5.83,17.105 5.995,16.995C6.105,16.94 6.38,16.995 6.545,17.05C6.71,17.215 6.875,17.435 6.985,17.655C7.315,18.095 7.755,18.15 8.195,17.82C8.69,17.38 8.965,16.775 9.13,16.17C9.46,14.85 9.79,13.585 10.065,12.265V12.045L7.15,11.605L7.26,10.78L10.285,11.22L10.67,9.515L7.535,9.02L7.645,8.14L10.89,8.58C11,8.25 11.055,7.975 11.165,7.7C11.44,6.71 11.715,5.72 12.375,4.84C13.035,3.96 13.805,3.41 14.96,3.465C15.455,3.465 15.95,3.63 16.28,4.015C16.335,4.07 16.445,4.18 16.445,4.29C16.445,4.51 16.445,4.785 16.28,4.95C16.06,5.115 15.785,5.06 15.565,4.84C15.4,4.675 15.29,4.51 15.125,4.345C14.795,3.905 14.3,3.85 13.915,4.235C13.64,4.51 13.365,4.895 13.2,5.28C12.815,6.435 12.54,7.645 12.155,8.855L15.18,9.295L14.96,10.12L12.045,9.68Z"
android:fillColor="#000000"
android:fillType="evenOdd" />
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:fillColor="#000000"
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z" />
<path
android:fillColor="#FCFC03"
android:pathData="M15.257,11.048L17.71,9.826L18.555,7.231L16.864,6H5.604L3,7.934H16.236L15.533,10.111H10.225L9.714,11.701H15.022L13.532,16.281L16.019,15.05L16.906,12.304L15.24,11.082L15.257,11.048Z" />
<path
android:fillColor="#FCFC03"
android:pathData="M6.742,14.313L8.275,9.541L6.575,8.269L4.021,16.281H13.532L14.168,14.313H6.742Z" />
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z" />
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#0090FF" />
<path
android:pathData="M12.045,9.68L11.715,11.44L14.85,11.88L14.63,12.705L11.55,12.265C11.33,12.98 11.22,13.75 10.945,14.41C10.67,15.18 10.395,15.95 10.065,16.665C9.625,17.6 8.855,18.26 7.81,18.425C7.205,18.535 6.545,18.48 6.05,18.095C5.885,17.985 5.72,17.765 5.72,17.6C5.72,17.38 5.83,17.105 5.995,16.995C6.105,16.94 6.38,16.995 6.545,17.05C6.71,17.215 6.875,17.435 6.985,17.655C7.315,18.095 7.755,18.15 8.195,17.82C8.69,17.38 8.965,16.775 9.13,16.17C9.46,14.85 9.79,13.585 10.065,12.265V12.045L7.15,11.605L7.26,10.78L10.285,11.22L10.67,9.515L7.535,9.02L7.645,8.14L10.89,8.58C11,8.25 11.055,7.975 11.165,7.7C11.44,6.71 11.715,5.72 12.375,4.84C13.035,3.96 13.805,3.41 14.96,3.465C15.455,3.465 15.95,3.63 16.28,4.015C16.335,4.07 16.445,4.18 16.445,4.29C16.445,4.51 16.445,4.785 16.28,4.95C16.06,5.115 15.785,5.06 15.565,4.84C15.4,4.675 15.29,4.51 15.125,4.345C14.795,3.905 14.3,3.85 13.915,4.235C13.64,4.51 13.365,4.895 13.2,5.28C12.815,6.435 12.54,7.645 12.155,8.855L15.18,9.295L14.96,10.12L12.045,9.68Z"
android:fillColor="#ffffff"
android:fillType="evenOdd" />
</group>
</vector>

View file

@ -0,0 +1,345 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="328dp"
android:height="78dp"
android:viewportWidth="328"
android:viewportHeight="78">
<group>
<clip-path
android:pathData="M14,0L314,0A14,14 0,0 1,328 14L328,64A14,14 0,0 1,314 78L14,78A14,14 0,0 1,0 64L0,14A14,14 0,0 1,14 0z"/>
<path
android:pathData="M14,0L314,0A14,14 0,0 1,328 14L328,64A14,14 0,0 1,314 78L14,78A14,14 0,0 1,0 64L0,14A14,14 0,0 1,14 0z"
android:fillColor="#010101"/>
<path
android:pathData="M0,0h328v78h-328z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="0"
android:startY="0"
android:endX="40.3"
android:endY="159.38"
android:type="linear">
<item android:offset="0" android:color="#FF0099FF"/>
<item android:offset="0.45" android:color="#00000000"/>
<item android:offset="0.6" android:color="#00000000"/>
<item android:offset="1" android:color="#FF0099FF"/>
</gradient>
</aapt:attr>
</path>
<group>
<clip-path
android:pathData="M255.81,48.61h38.44v17.39h-38.44z"/>
<path
android:pathData="M294.04,55.66C294.04,52.02 285.53,49.07 275.03,49.07C264.54,49.07 256.03,52.02 256.03,55.66V59.19C256.03,62.83 264.54,65.78 275.03,65.78C285.53,65.78 294.04,62.83 294.04,59.19V55.66Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="255.89"
android:startY="57.42"
android:endX="294.04"
android:endY="57.42"
android:type="linear">
<item android:offset="0" android:color="#FF8F929E"/>
<item android:offset="0.26" android:color="#FF6A6D7A"/>
<item android:offset="0.5" android:color="#FF5E616D"/>
<item android:offset="0.74" android:color="#FF80828F"/>
<item android:offset="1" android:color="#FF9295A1"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M275.03,66C264.26,66 255.81,63.01 255.81,59.19V55.66C255.81,51.84 264.26,48.85 275.03,48.85C285.81,48.85 294.25,51.84 294.25,55.66V59.19C294.25,63.01 285.81,66 275.03,66ZM275.03,49.29C264.67,49.29 256.25,52.15 256.25,55.66V59.19C256.25,62.7 264.67,65.56 275.03,65.56C285.39,65.56 293.82,62.7 293.82,59.19V55.66C293.82,52.15 285.39,49.29 275.03,49.29V49.29Z"
android:fillColor="#727481"/>
<path
android:pathData="M275.03,62.25C285.53,62.25 294.04,59.3 294.04,55.66C294.04,52.02 285.53,49.07 275.03,49.07C264.54,49.07 256.03,52.02 256.03,55.66C256.03,59.3 264.54,62.25 275.03,62.25Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="273.96"
android:startY="49.68"
android:endX="275.63"
android:endY="58.7"
android:type="linear">
<item android:offset="0" android:color="#FFE1E3F0"/>
<item android:offset="1" android:color="#FFA8AAB7"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M275.03,62.48C264.26,62.48 255.81,59.49 255.81,55.66C255.81,51.84 264.26,48.85 275.03,48.85C285.81,48.85 294.25,51.84 294.25,55.66C294.25,59.48 285.81,62.47 275.03,62.47V62.48ZM275.03,49.29C264.67,49.29 256.25,52.15 256.25,55.66C256.25,59.17 264.67,62.03 275.03,62.03C285.39,62.03 293.82,59.18 293.82,55.66C293.82,52.15 285.39,49.29 275.03,49.29Z"
android:fillColor="#F3F6FF"/>
<path
android:pathData="M293.95,55.02C293.01,51.68 284.9,49.07 275.03,49.07C265.17,49.07 257.06,51.68 256.12,55.02H293.95Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="271.42"
android:startY="41.66"
android:endX="276.67"
android:endY="60.45"
android:type="linear">
<item android:offset="0.2" android:color="#FFFFFFFF"/>
<item android:offset="1" android:color="#FFFFFFFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M275.03,49.95C266.17,49.95 258.99,52.48 258.99,55.59C258.99,56.77 260.02,57.86 261.77,58.76C260.47,57.98 259.72,57.07 259.72,56.11C259.72,53.17 266.58,50.79 275.03,50.79C283.49,50.79 290.35,53.17 290.35,56.11C290.35,57.08 289.6,57.98 288.29,58.76C290.04,57.86 291.07,56.77 291.07,55.59C291.07,52.48 283.89,49.96 275.03,49.96V49.95Z"
android:fillColor="#8D909C"/>
<path
android:pathData="M259.1,55.66C259.1,55.66 259.1,55.81 259.15,56.06C259.21,56.32 259.35,56.69 259.66,57.07C260.27,57.84 261.48,58.59 263.04,59.18C264.58,59.79 266.49,60.23 268.54,60.54C270.59,60.86 272.81,61.01 275.03,61C277.25,61 279.46,60.85 281.52,60.54C283.58,60.23 285.48,59.79 287.03,59.18C288.58,58.59 289.79,57.84 290.4,57.07C290.71,56.69 290.85,56.32 290.91,56.06C290.95,55.94 290.95,55.84 290.95,55.76C290.96,55.69 290.96,55.66 290.96,55.66C290.96,55.66 290.96,55.7 290.96,55.76C290.95,55.84 290.96,55.94 290.92,56.06C290.88,56.32 290.75,56.7 290.45,57.1C289.86,57.91 288.67,58.75 287.1,59.38C285.54,60.02 283.64,60.53 281.57,60.86C279.5,61.19 277.27,61.37 275.03,61.37C272.8,61.37 270.56,61.19 268.49,60.86C266.42,60.53 264.51,60.02 262.96,59.38C261.4,58.75 260.21,57.91 259.61,57.1C259.32,56.71 259.18,56.32 259.14,56.06C259.11,55.93 259.11,55.83 259.11,55.76C259.11,55.69 259.1,55.66 259.1,55.66Z"
android:fillColor="#D4D6E3"/>
<path
android:pathData="M275.4,48.77C275.3,48.67 275.18,48.62 275.03,48.62C274.3,48.57 274.28,49.67 275.01,49.62C275.08,49.62 275.16,49.61 275.23,49.58C275.23,49.58 275.57,49.38 275.39,49.46C275.67,49.33 275.71,48.91 275.4,48.77Z"
android:fillColor="#ffffff"/>
</group>
<group>
<clip-path
android:pathData="M254.9,44.03h38.44v17.39h-38.44z"/>
<path
android:pathData="M293.12,51.09C293.12,47.45 284.61,44.49 274.12,44.49C263.63,44.49 255.12,47.45 255.12,51.09V54.61C255.12,58.25 263.63,61.2 274.12,61.2C284.61,61.2 293.12,58.25 293.12,54.61V51.09Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="254.97"
android:startY="52.85"
android:endX="293.13"
android:endY="52.85"
android:type="linear">
<item android:offset="0" android:color="#FF8F929E"/>
<item android:offset="0.26" android:color="#FF6A6D7A"/>
<item android:offset="0.5" android:color="#FF5E616D"/>
<item android:offset="0.74" android:color="#FF80828F"/>
<item android:offset="1" android:color="#FF9295A1"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M274.12,61.42C263.34,61.42 254.9,58.43 254.9,54.61V51.09C254.9,47.27 263.34,44.27 274.12,44.27C284.89,44.27 293.34,47.27 293.34,51.09V54.61C293.34,58.43 284.89,61.42 274.12,61.42ZM274.12,44.72C263.76,44.72 255.33,47.58 255.33,51.09V54.61C255.33,58.13 263.76,60.98 274.12,60.98C284.48,60.98 292.91,58.13 292.91,54.61V51.09C292.91,47.57 284.48,44.71 274.12,44.71V44.72Z"
android:fillColor="#727481"/>
<path
android:pathData="M274.12,57.68C284.61,57.68 293.12,54.72 293.12,51.09C293.12,47.45 284.61,44.49 274.12,44.49C263.62,44.49 255.12,47.45 255.12,51.09C255.12,54.72 263.62,57.68 274.12,57.68Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="273.04"
android:startY="45.1"
android:endX="274.72"
android:endY="54.12"
android:type="linear">
<item android:offset="0" android:color="#FFE1E3F0"/>
<item android:offset="1" android:color="#FFA8AAB7"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M274.12,57.9C263.34,57.9 254.9,54.91 254.9,51.09C254.9,47.26 263.34,44.27 274.12,44.27C284.89,44.27 293.34,47.27 293.34,51.09C293.34,54.9 284.89,57.9 274.12,57.9V57.9ZM274.12,44.72C263.76,44.72 255.33,47.58 255.33,51.09C255.33,54.59 263.76,57.46 274.12,57.46C284.48,57.46 292.91,54.6 292.91,51.09C292.91,47.57 284.48,44.72 274.12,44.72Z"
android:fillColor="#F3F6FF"/>
<path
android:pathData="M293.03,50.45C292.1,47.1 283.99,44.49 274.12,44.49C264.25,44.49 256.14,47.1 255.21,50.45H293.03Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="270.5"
android:startY="37.09"
android:endX="275.75"
android:endY="55.87"
android:type="linear">
<item android:offset="0.2" android:color="#FFFFFFFF"/>
<item android:offset="1" android:color="#FFFFFFFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M274.12,45.37C265.26,45.37 258.08,47.9 258.08,51.01C258.08,52.19 259.1,53.28 260.86,54.19C259.55,53.41 258.8,52.5 258.8,51.53C258.8,48.6 265.66,46.21 274.12,46.21C282.57,46.21 289.43,48.6 289.43,51.53C289.43,52.5 288.68,53.41 287.37,54.19C289.13,53.28 290.16,52.19 290.16,51.01C290.16,47.9 282.98,45.38 274.12,45.38V45.37Z"
android:fillColor="#8D909C"/>
<path
android:pathData="M258.19,51.09C258.19,51.09 258.19,51.23 258.24,51.49C258.29,51.74 258.44,52.11 258.75,52.49C259.36,53.27 260.57,54.01 262.12,54.61C263.67,55.21 265.57,55.66 267.62,55.97C269.68,56.28 271.9,56.43 274.11,56.42C276.33,56.43 278.55,56.28 280.61,55.97C282.66,55.65 284.56,55.21 286.11,54.61C287.66,54.01 288.88,53.27 289.48,52.49C289.8,52.11 289.94,51.74 289.99,51.49C290.03,51.36 290.03,51.26 290.04,51.19C290.04,51.12 290.05,51.09 290.05,51.09C290.05,51.09 290.05,51.12 290.04,51.19C290.04,51.26 290.04,51.36 290.01,51.49C289.97,51.75 289.83,52.13 289.53,52.53C288.94,53.34 287.75,54.17 286.19,54.8C284.63,55.44 282.73,55.96 280.66,56.29C278.59,56.62 276.35,56.79 274.12,56.8C271.88,56.79 269.65,56.62 267.58,56.29C265.5,55.96 263.6,55.44 262.05,54.8C260.49,54.17 259.29,53.34 258.7,52.53C258.4,52.13 258.27,51.75 258.22,51.49C258.19,51.36 258.2,51.25 258.19,51.19C258.19,51.12 258.19,51.09 258.19,51.09Z"
android:fillColor="#D4D6E3"/>
<path
android:pathData="M274.49,44.2C274.39,44.09 274.27,44.04 274.11,44.04C273.38,43.99 273.37,45.09 274.09,45.04C274.17,45.04 274.24,45.03 274.32,45C274.32,45 274.65,44.8 274.47,44.89C274.75,44.76 274.79,44.33 274.49,44.2Z"
android:fillColor="#ffffff"/>
</group>
<group>
<clip-path
android:pathData="M255.81,39.46h38.44v18.31h-38.44z"/>
<path
android:pathData="M294.04,46.88C294.04,43.05 285.53,39.94 275.03,39.94C264.54,39.94 256.03,43.05 256.03,46.88V50.59C256.03,54.42 264.54,57.53 275.03,57.53C285.53,57.53 294.04,54.42 294.04,50.59V46.88Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="255.89"
android:startY="48.74"
android:endX="294.04"
android:endY="48.74"
android:type="linear">
<item android:offset="0" android:color="#FF8F929E"/>
<item android:offset="0.26" android:color="#FF6A6D7A"/>
<item android:offset="0.5" android:color="#FF5E616D"/>
<item android:offset="0.74" android:color="#FF80828F"/>
<item android:offset="1" android:color="#FF9295A1"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M275.03,57.76C264.26,57.76 255.81,54.62 255.81,50.59V46.88C255.81,42.86 264.26,39.71 275.03,39.71C285.81,39.71 294.25,42.86 294.25,46.88V50.59C294.25,54.61 285.81,57.76 275.03,57.76ZM275.03,40.18C264.67,40.18 256.25,43.19 256.25,46.88V50.59C256.25,54.29 264.67,57.3 275.03,57.3C285.39,57.3 293.82,54.29 293.82,50.59V46.88C293.82,43.18 285.39,40.17 275.03,40.17V40.18Z"
android:fillColor="#727481"/>
<path
android:pathData="M275.03,53.82C285.53,53.82 294.04,50.71 294.04,46.88C294.04,43.05 285.53,39.94 275.03,39.94C264.54,39.94 256.03,43.05 256.03,46.88C256.03,50.71 264.54,53.82 275.03,53.82Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="273.96"
android:startY="40.58"
android:endX="275.81"
android:endY="50.04"
android:type="linear">
<item android:offset="0" android:color="#FFE1E3F0"/>
<item android:offset="1" android:color="#FFA8AAB7"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M275.03,54.06C264.26,54.06 255.81,50.91 255.81,46.88C255.81,42.86 264.26,39.71 275.03,39.71C285.81,39.71 294.25,42.86 294.25,46.88C294.25,50.9 285.81,54.05 275.03,54.05V54.06ZM275.03,40.18C264.67,40.18 256.25,43.19 256.25,46.88C256.25,50.57 264.67,53.59 275.03,53.59C285.39,53.59 293.82,50.58 293.82,46.88C293.82,43.18 285.39,40.18 275.03,40.18Z"
android:fillColor="#F3F6FF"/>
<path
android:pathData="M293.95,46.21C293.01,42.69 284.9,39.94 275.03,39.94C265.17,39.94 257.06,42.69 256.12,46.21H293.95Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="271.42"
android:startY="32.14"
android:endX="277.19"
android:endY="51.77"
android:type="linear">
<item android:offset="0.2" android:color="#FFFFFFFF"/>
<item android:offset="1" android:color="#FFFFFFFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M275.03,40.87C266.17,40.87 258.99,43.53 258.99,46.81C258.99,48.05 260.02,49.19 261.77,50.15C260.47,49.32 259.72,48.36 259.72,47.35C259.72,44.26 266.58,41.75 275.03,41.75C283.49,41.75 290.35,44.26 290.35,47.35C290.35,48.37 289.6,49.32 288.29,50.15C290.04,49.19 291.07,48.05 291.07,46.81C291.07,43.53 283.89,40.87 275.03,40.87V40.87Z"
android:fillColor="#8D909C"/>
<path
android:pathData="M259.1,46.88C259.1,46.88 259.1,47.03 259.15,47.3C259.21,47.57 259.35,47.96 259.66,48.36C260.27,49.18 261.48,49.96 263.04,50.59C264.58,51.23 266.49,51.69 268.54,52.02C270.59,52.35 272.81,52.51 275.03,52.5C277.25,52.5 279.46,52.34 281.52,52.02C283.58,51.69 285.48,51.23 287.03,50.59C288.58,49.96 289.79,49.18 290.4,48.36C290.71,47.96 290.85,47.57 290.91,47.3C290.95,47.17 290.95,47.06 290.95,46.99C290.96,46.91 290.96,46.88 290.96,46.88C290.96,46.88 290.96,46.92 290.96,46.99C290.95,47.06 290.96,47.17 290.92,47.3C290.88,47.58 290.75,47.98 290.45,48.4C289.86,49.25 288.67,50.13 287.1,50.79C285.54,51.47 283.64,52.01 281.57,52.36C279.5,52.7 277.27,52.89 275.03,52.89C272.8,52.89 270.56,52.7 268.49,52.36C266.42,52.01 264.51,51.47 262.96,50.79C261.4,50.13 260.21,49.25 259.61,48.4C259.32,47.98 259.18,47.58 259.14,47.3C259.11,47.17 259.11,47.06 259.11,46.99C259.11,46.91 259.1,46.88 259.1,46.88Z"
android:fillColor="#D4D6E3"/>
</group>
<group>
<clip-path
android:pathData="M248.49,25.73h37.53v22.88h-37.53z"/>
<path
android:pathData="M284.27,28.12C282.88,24.76 273.83,25.37 264.06,29.47C254.29,33.57 247.5,39.62 248.89,42.97C249.12,43.53 250,45.66 250.24,46.22C251.63,49.58 260.67,48.97 270.45,44.87C280.22,40.77 287.01,34.72 285.62,31.37C285.39,30.81 284.5,28.68 284.27,28.12Z"
android:strokeAlpha="0.5"
android:fillAlpha="0.5">
<aapt:attr name="android:fillColor">
<gradient
android:startX="251.72"
android:startY="43.7"
android:endX="281.28"
android:endY="31.45"
android:type="linear">
<item android:offset="0" android:color="#FF8F929E"/>
<item android:offset="0.26" android:color="#FF767885"/>
<item android:offset="0.5" android:color="#FF6C6F7C"/>
<item android:offset="0.76" android:color="#FF767986"/>
<item android:offset="0.77" android:color="#FF858794"/>
<item android:offset="1" android:color="#FF9295A1"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M255.83,48.61C252.66,48.61 250.67,47.81 250.04,46.3L248.69,43.06C247.98,41.34 249.21,38.95 252.15,36.33C255.03,33.76 259.23,31.26 263.98,29.26C269.33,27.02 274.69,25.73 278.68,25.73C281.85,25.73 283.85,26.53 284.48,28.04L285.82,31.29C286.53,33.01 285.31,35.39 282.37,38.01C279.48,40.58 275.28,43.09 270.53,45.08C265.18,47.33 259.82,48.62 255.83,48.62V48.61ZM278.68,26.17C274.75,26.17 269.45,27.45 264.15,29.67C259.44,31.64 255.29,34.13 252.44,36.66C249.69,39.11 248.47,41.38 249.1,42.88L250.44,46.13C250.99,47.45 252.9,48.17 255.83,48.17C259.77,48.17 265.06,46.9 270.37,44.67C275.07,42.7 279.23,40.21 282.08,37.68C284.83,35.23 286.04,32.96 285.42,31.45L284.07,28.2C283.53,26.89 281.62,26.16 278.68,26.16V26.17Z"
android:strokeAlpha="0.5"
android:fillColor="#727481"
android:fillAlpha="0.5"/>
<path
android:pathData="M248.89,42.97C250.28,46.33 259.33,45.72 269.1,41.62C278.87,37.52 285.66,31.47 284.27,28.12C282.88,24.76 273.83,25.37 264.06,29.47C254.29,33.57 247.5,39.62 248.89,42.97Z"
android:strokeAlpha="0.5"
android:fillAlpha="0.5">
<aapt:attr name="android:fillColor">
<gradient
android:startX="268.95"
android:startY="41.25"
android:endX="263.22"
android:endY="27.6"
android:type="linear">
<item android:offset="0" android:color="#FFE1E3F0"/>
<item android:offset="1" android:color="#FFA8AAB7"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M254.48,45.36C251.31,45.36 249.32,44.56 248.68,43.05C247.97,41.33 249.2,38.94 252.14,36.33C255.02,33.76 259.23,31.25 263.98,29.26C269.33,27.02 274.69,25.73 278.68,25.73C281.85,25.73 283.85,26.53 284.48,28.04C285.19,29.75 283.96,32.14 281.02,34.76C278.14,37.33 273.93,39.84 269.19,41.83C263.83,44.08 258.47,45.36 254.48,45.36L254.48,45.36ZM278.68,26.17C274.75,26.17 269.45,27.45 264.15,29.67C259.44,31.64 255.29,34.13 252.44,36.66C249.69,39.11 248.47,41.38 249.09,42.88C249.64,44.19 251.55,44.92 254.48,44.92C258.42,44.92 263.72,43.65 269.02,41.42C273.73,39.45 277.88,36.96 280.73,34.42C283.48,31.98 284.7,29.71 284.07,28.2C283.53,26.89 281.62,26.16 278.68,26.16V26.17Z"
android:strokeAlpha="0.5"
android:fillColor="#F3F6FF"
android:fillAlpha="0.5"/>
<path
android:pathData="M258.69,44.75C261.81,44.23 265.4,43.18 269.1,41.63C278.76,37.57 285.5,31.62 284.31,28.24C264.01,31.71 254,36.96 258.69,44.75Z"
android:strokeAlpha="0.5"
android:fillAlpha="0.5">
<aapt:attr name="android:fillColor">
<gradient
android:startX="274.55"
android:startY="43.99"
android:endX="268.53"
android:endY="29.66"
android:type="linear">
<item android:offset="0.2" android:color="#FFFFFFFF"/>
<item android:offset="1" android:color="#FFFFFFFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M264.4,30.28C256.15,33.74 250.43,38.87 251.62,41.75C252.07,42.83 253.45,43.44 255.42,43.58C253.91,43.37 252.86,42.83 252.49,41.93C251.37,39.23 256.85,34.35 264.72,31.05C272.6,27.74 279.89,27.26 281.01,29.97C281.38,30.86 281.02,31.99 280.11,33.22C281.39,31.7 281.94,30.29 281.48,29.21C280.29,26.34 272.64,26.82 264.4,30.27L264.4,30.28Z"
android:strokeAlpha="0.5"
android:fillColor="#8D909C"
android:fillAlpha="0.5"/>
</group>
<group>
<clip-path
android:pathData="M243,12h30.2v32.95h-30.2z"/>
<path
android:pathData="M269.73,12.97C267.2,10.4 259.39,14.69 252.29,22.57C245.19,30.45 241.49,38.91 244.01,41.49C244.44,41.92 246.04,43.55 246.46,43.98C249,46.55 256.81,42.25 263.91,34.38C271.02,26.51 274.72,18.04 272.18,15.47C271.76,15.03 270.16,13.4 269.73,12.98V12.97Z"
android:strokeAlpha="0.25"
android:fillAlpha="0.25">
<aapt:attr name="android:fillColor">
<gradient
android:startX="246.82"
android:startY="41.01"
android:endX="269.6"
android:endY="18.59"
android:type="linear">
<item android:offset="0" android:color="#FFA6A8B5"/>
<item android:offset="0.16" android:color="#FFD2D5E2"/>
<item android:offset="0.17" android:color="#FFDEE1ED"/>
<item android:offset="0.48" android:color="#FFFFFFFF"/>
<item android:offset="0.8" android:color="#FFD6D9E6"/>
<item android:offset="1" android:color="#FFA3A6B3"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M248.53,44.95C247.59,44.95 246.85,44.68 246.32,44.14L243.87,41.65C241.21,38.95 244.85,30.5 252.14,22.41C257.76,16.19 264,12 267.68,12C268.61,12 269.35,12.27 269.88,12.81L272.33,15.3C274.99,18 271.36,26.45 264.06,34.54C258.44,40.76 252.2,44.95 248.52,44.95H248.53ZM267.68,12.45C264.11,12.45 257.99,16.58 252.45,22.72C245.43,30.5 241.72,38.84 244.16,41.32L246.61,43.81C247.05,44.27 247.7,44.49 248.52,44.49C252.09,44.49 258.21,40.36 263.75,34.22C270.76,26.45 274.48,18.1 272.03,15.62L269.58,13.13C269.14,12.68 268.49,12.45 267.67,12.45H267.68Z"
android:strokeAlpha="0.25"
android:fillColor="#9EA0AD"
android:fillAlpha="0.25"/>
<path
android:pathData="M244.01,41.49C246.55,44.06 254.36,39.77 261.46,31.89C268.57,24.01 272.27,15.54 269.73,12.97C267.2,10.4 259.39,14.69 252.29,22.57C245.19,30.45 241.49,38.91 244.01,41.49Z"
android:strokeAlpha="0.25"
android:fillAlpha="0.25">
<aapt:attr name="android:fillColor">
<gradient
android:startX="253.84"
android:startY="24.14"
android:endX="259.77"
android:endY="29.49"
android:type="linear">
<item android:offset="0" android:color="#FF8A8C99"/>
<item android:offset="1" android:color="#FFA8AAB7"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M246.08,42.46C245.14,42.46 244.4,42.19 243.87,41.65C241.21,38.95 244.85,30.5 252.14,22.41C257.76,16.19 264,12 267.68,12C268.61,12 269.35,12.27 269.88,12.81C271.18,14.12 271.01,16.87 269.41,20.54C267.83,24.13 265.07,28.22 261.62,32.05C256,38.27 249.76,42.46 246.08,42.46ZM267.68,12.45C264.11,12.45 257.99,16.58 252.45,22.72C245.43,30.5 241.72,38.84 244.16,41.32C244.6,41.78 245.25,42.01 246.07,42.01C249.64,42.01 255.76,37.87 261.3,31.73C264.73,27.94 267.46,23.9 269.01,20.34C270.51,16.92 270.72,14.28 269.58,13.13C269.14,12.68 268.49,12.45 267.67,12.45H267.68Z"
android:strokeAlpha="0.25"
android:fillColor="#F3F6FF"
android:fillAlpha="0.25"/>
<path
android:pathData="M252.9,23.19C246.91,29.84 243.8,37.01 245.97,39.21C246.79,40.04 248.24,40.04 250.06,39.37C248.64,39.79 247.49,39.72 246.82,39.03C244.78,36.95 247.76,30.13 253.49,23.78C259.21,17.44 265.51,13.97 267.55,16.05C268.22,16.73 268.35,17.94 268,19.47C268.56,17.52 268.5,15.98 267.68,15.14C265.51,12.94 258.9,16.55 252.9,23.19H252.9Z"
android:strokeAlpha="0.25"
android:fillColor="#D7DAE6"
android:fillAlpha="0.25"/>
</group>
</group>
</vector>

View file

@ -0,0 +1,35 @@
package com.tangem.utils
import java.util.Locale
object SupportedLanguages {
const val ENGLISH = "en"
const val RUSSIAN = "ru"
const val GERMAN = "de"
const val FRANCH = "fr"
const val ITALIAN = "it"
const val JAPANESE = "ja"
const val UKRAINIAN = "uk"
const val CHINESE = "uk"
val supportedLangugeCodes = listOf(
ENGLISH,
RUSSIAN,
GERMAN,
FRANCH,
ITALIAN,
JAPANESE,
UKRAINIAN,
CHINESE,
)
fun getCurrentSupportedLanguageCode(): String {
val locale = Locale.getDefault()
return if (supportedLangugeCodes.contains(locale.language)) {
locale.language
} else {
ENGLISH
}
}
}