Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-16 14:59:49 +03:00
commit 479d554dae
14 changed files with 122 additions and 53 deletions

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@ -25,7 +26,6 @@ import coil.request.ImageRequest
import com.tangem.core.ui.components.CurrencyPlaceholderIcon
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
import com.tangem.tap.common.compose.extensions.toPx
import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState
import com.tangem.wallet.R
import kotlinx.coroutines.launch
@ -126,29 +126,37 @@ internal fun TokenItem(model: TokenItemState) {
@Composable
private fun Icon(name: String, iconUrl: String, onContrastCalculate: (Color) -> Unit, modifier: Modifier = Modifier) {
val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size46.roundToPx() }
val iconModifier = modifier
.size(size = TangemTheme.dimens.size46)
.clip(TangemTheme.shapes.roundedCorners8)
val screenBackgroundColor = TangemTheme.colors.background.primary.toArgb()
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
var isBackgroundColorDefined by remember { mutableStateOf(false) }
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
val isDarkTheme = isSystemInDarkTheme()
val coroutineScope = rememberCoroutineScope()
SubcomposeAsyncImage(
modifier = iconModifier,
model = ImageRequest.Builder(context = LocalContext.current)
.size(size = TangemTheme.dimens.size46.toPx().toInt())
.data(data = iconUrl)
.size(size = pixelsSize)
.memoryCacheKey(key = iconUrl + pixelsSize)
.crossfade(enable = true)
.allowHardware(false)
.listener(
onSuccess = { _, result ->
if (isDarkTheme) {
if (!isBackgroundColorDefined && isDarkTheme) {
coroutineScope.launch {
val color = ImageBackgroundContrastChecker(
drawable = result.drawable,
backgroundColor = screenBackgroundColor,
).getContrastColorIfNeeded(isDarkTheme)
backgroundColor = itemBackgroundColor,
size = pixelsSize,
).getContrastColor(isDarkTheme = true)
onContrastCalculate(color)
iconBackgroundColor = color
isBackgroundColorDefined = true
}
}
},
@ -189,13 +197,12 @@ private fun Subtitle(isExpanded: Boolean, modifier: Modifier = Modifier) {
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun ChangeNetworksViewButton(isExpanded: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
IconButton(onClick = onClick, modifier = modifier.size(size = TangemTheme.dimens.size46)) {
AnimatedContent(
targetState = isExpanded,
transitionSpec = { fadeIn() + scaleIn() with scaleOut() + fadeOut() },
transitionSpec = { (fadeIn() + scaleIn()).togetherWith(scaleOut() + fadeOut()) },
) { isExpanded ->
Icon(
painter = painterResource(

View file

@ -4,7 +4,7 @@ import com.tangem.common.extensions.toHexString
import com.tangem.lib.auth.AuthProvider
import com.tangem.tap.proxy.AppStateHolder
class AuthProviderImpl(private val appStateHolder: AppStateHolder) : AuthProvider {
internal class DefaultAuthProvider(private val appStateHolder: AppStateHolder) : AuthProvider {
override fun getCardPublicKey(): String {
return appStateHolder.scanResponse?.card?.cardPublicKey?.toHexString() ?: ""

View file

@ -0,0 +1,32 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.sessionId.ExpressSessionIdGenerator
import java.util.UUID
import java.util.concurrent.atomic.AtomicReference
internal class DefaultExpressAuthProvider(
private val userWalletsStore: UserWalletsStore,
private val configManager: ConfigManager,
) : ExpressAuthProvider, ExpressSessionIdGenerator {
private var uuid = AtomicReference(UUID.randomUUID())
override fun getApiKey(): String {
return configManager.config.tangemExpressApiKey
}
override fun getUserId(): String {
return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: ""
}
override fun getSessionId(): String {
return uuid.get().toString()
}
override fun generateNewSessionId() {
uuid = AtomicReference(UUID.randomUUID())
}
}

View file

@ -4,8 +4,8 @@ import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.lib.auth.AuthProvider
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.tap.network.auth.AuthProviderImpl
import com.tangem.tap.network.auth.ExpressAuthProviderImpl
import com.tangem.tap.network.auth.DefaultAuthProvider
import com.tangem.tap.network.auth.DefaultExpressAuthProvider
import com.tangem.tap.proxy.AppStateHolder
import dagger.Module
import dagger.Provides
@ -20,7 +20,7 @@ class AuthModule {
@Provides
@Singleton
fun provideAuthProvider(appStateHolder: AppStateHolder): AuthProvider {
return AuthProviderImpl(appStateHolder)
return DefaultAuthProvider(appStateHolder)
}
@Provides
@ -29,7 +29,7 @@ class AuthModule {
userWalletsStore: UserWalletsStore,
configManager: ConfigManager,
): ExpressAuthProvider {
return ExpressAuthProviderImpl(
return DefaultExpressAuthProvider(
userWalletsStore = userWalletsStore,
configManager = configManager,
)

View file

@ -6,7 +6,6 @@ import com.tangem.datasource.api.express.models.request.PairsRequestBody
import com.tangem.datasource.api.express.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
import java.math.BigDecimal
@ -17,14 +16,8 @@ import java.math.BigDecimal
@Suppress("LongParameterList")
interface ExpressApi {
// TODO move first three params to retrofit interceptor
@POST("assets")
suspend fun getAssets(
@Header("api-key") apiKey: String,
@Header("user-id") userId: String,
@Header("session-id") sessionId: String,
@Body body: AssetsRequestBody,
): ApiResponse<List<Asset>>
suspend fun getAssets(@Body body: AssetsRequestBody): ApiResponse<List<Asset>>
@POST("pairs")
suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse<List<SwapPair>>
@ -55,6 +48,6 @@ interface ExpressApi {
@Query("toAddress") toAddress: String,
): ApiResponse<ExchangeDataResponse>
@GET("exchange-results")
@GET("exchange-result")
suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse<ExchangeResultsResponse>
}

View file

@ -97,7 +97,7 @@ class NetworkModule {
}
private companion object {
const val PROD_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]"
const val PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/"
const val DEV_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]"
const val PROD_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/"

View file

@ -9,6 +9,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon
@ -19,16 +21,19 @@ import kotlinx.coroutines.launch
@Composable
internal inline fun DefaultCurrencyIcon(
iconData: Any,
size: Dp,
alpha: Float,
colorFilter: ColorFilter?,
crossinline errorIcon: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
var isBackgroundColorDefined by remember { mutableStateOf(false) }
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
val isDarkTheme = isSystemInDarkTheme()
val coroutineScope = rememberCoroutineScope()
val pixelsSize = with(LocalDensity.current) { size.roundToPx() }
SubcomposeAsyncImage(
modifier = modifier
.background(
@ -38,17 +43,21 @@ internal inline fun DefaultCurrencyIcon(
.clip(TangemTheme.shapes.roundedCorners8),
model = ImageRequest.Builder(context = LocalContext.current)
.data(iconData)
.size(size = pixelsSize)
.memoryCacheKey(key = iconData.toString() + pixelsSize)
.crossfade(enable = true)
.allowHardware(false)
.allowHardware(enable = false)
.listener(
onSuccess = { _, result ->
if (isDarkTheme) {
if (!isBackgroundColorDefined && isDarkTheme) {
coroutineScope.launch {
val color = ImageBackgroundContrastChecker(
drawable = result.drawable,
backgroundColor = itemBackgroundColor,
).getContrastColorIfNeeded(isDarkTheme)
size = pixelsSize,
).getContrastColor(isDarkTheme = true)
iconBackgroundColor = color
isBackgroundColorDefined = true
}
}
},

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
@ -18,21 +19,23 @@ import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
@Composable
fun FiatIcon(
url: String?,
size: Dp,
modifier: Modifier = Modifier,
@DrawableRes fallbackResId: Int = R.drawable.ic_shape_circle,
) {
val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url
DefaultCurrencyIcon(
modifier = modifier,
iconData = iconData,
size = size,
alpha = 1f,
colorFilter = null,
errorIcon = {
Image(
painter = painterResource(id = fallbackResId),
contentDescription = null,
)
},
alpha = 1f,
colorFilter = null,
modifier = modifier,
)
}

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun ContentIcon(
@ -67,8 +68,10 @@ private fun CoinIcon(
val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url
DefaultCurrencyIcon(
modifier = modifier,
iconData = iconData,
size = TangemTheme.dimens.size36,
alpha = alpha,
colorFilter = colorFilter,
errorIcon = {
Image(
painter = painterResource(id = fallbackResId),
@ -77,8 +80,7 @@ private fun CoinIcon(
contentDescription = null,
)
},
alpha = alpha,
colorFilter = colorFilter,
modifier = modifier,
)
}
@ -94,11 +96,12 @@ private fun TokenIcon(
errorIcon()
} else {
DefaultCurrencyIcon(
modifier = modifier,
iconData = url,
errorIcon = errorIcon,
size = TangemTheme.dimens.size36,
alpha = alpha,
colorFilter = colorFilter,
errorIcon = errorIcon,
modifier = modifier,
)
}
}

View file

@ -13,12 +13,12 @@ class ImageBackgroundContrastChecker(
private val image: Bitmap,
private val backgroundColor: Int,
) {
constructor(drawable: Drawable, backgroundColor: Int) : this(
image = drawable.toBitmap(),
constructor(drawable: Drawable, backgroundColor: Int, size: Int) : this(
image = drawable.toBitmap(width = size, height = size),
backgroundColor = backgroundColor,
)
suspend fun getContrastColorIfNeeded(isDarkTheme: Boolean): Color {
suspend fun getContrastColor(isDarkTheme: Boolean): Color {
return if (isLowContrast()) {
if (isDarkTheme) Color.White else Color.Black
} else {
@ -26,7 +26,7 @@ class ImageBackgroundContrastChecker(
}
}
suspend fun isLowContrast(): Boolean {
private suspend fun isLowContrast(): Boolean {
val palette = generatePaletteAsync(bitmap = image)
val color = palette.getDominantColor(backgroundColor)
val contrast = ColorUtils.calculateContrast(color, backgroundColor)

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
@ -25,8 +26,7 @@ import kotlinx.coroutines.launch
@Composable
internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) {
val iconModifier = modifier
.size(TangemTheme.dimens.size36)
val iconModifier = modifier.size(TangemTheme.dimens.size36)
if (state.iconReference != null) {
DefaultCurrencyIcon(
modifier = iconModifier,
@ -73,11 +73,14 @@ private inline fun DefaultCurrencyIcon(
crossinline errorIcon: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
var isBackgroundColorDefined by remember { mutableStateOf(false) }
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
val isDarkTheme = isSystemInDarkTheme()
val coroutineScope = rememberCoroutineScope()
val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }
SubcomposeAsyncImage(
modifier = modifier
.background(
@ -86,17 +89,21 @@ private inline fun DefaultCurrencyIcon(
),
model = ImageRequest.Builder(context = LocalContext.current)
.data(iconReference.getReference())
.size(size = pixelsSize)
.memoryCacheKey(key = iconReference.getReference().toString() + pixelsSize)
.crossfade(enable = true)
.allowHardware(false)
.listener(
onSuccess = { _, result ->
if (isDarkTheme) {
if (!isBackgroundColorDefined && isDarkTheme) {
coroutineScope.launch {
val color = ImageBackgroundContrastChecker(
drawable = result.drawable,
backgroundColor = itemBackgroundColor,
).getContrastColorIfNeeded(isDarkTheme)
size = pixelsSize,
).getContrastColor(isDarkTheme = true)
iconBackgroundColor = color
isBackgroundColorDefined = true
}
}
},

View file

@ -15,6 +15,7 @@ import com.tangem.core.ui.components.currency.fiaticon.FiatIcon
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
@ -72,8 +73,8 @@ private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) {
if (button.isFiat) {
FiatIcon(
url = button.iconUrl,
modifier = Modifier
.size(TangemTheme.dimens.size18),
size = TangemTheme.dimens.size18,
modifier = Modifier.size(TangemTheme.dimens.size18),
)
} else {
button.iconState?.let {

View file

@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@ -292,8 +293,9 @@ private fun TokenIcon(
@DrawableRes iconPlaceholder: Int? = null,
@DrawableRes networkIconRes: Int? = null,
) {
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
var isBackgroundColorDefined by remember { mutableStateOf(false) }
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
val isDarkTheme = isSystemInDarkTheme()
val coroutineScope = rememberCoroutineScope()
@ -310,13 +312,16 @@ private fun TokenIcon(
shape = TangemTheme.shapes.roundedCorners8,
)
val data = tokenIconUrl.ifEmpty {
iconPlaceholder
}
val data = tokenIconUrl.ifEmpty { iconPlaceholder }
val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }
SubcomposeAsyncImage(
modifier = tokenImageModifier,
model = ImageRequest.Builder(LocalContext.current)
.data(data)
.size(size = pixelsSize)
.memoryCacheKey(key = data.toString() + pixelsSize)
.crossfade(true)
.allowHardware(false)
.listener(
@ -326,8 +331,10 @@ private fun TokenIcon(
val color = ImageBackgroundContrastChecker(
drawable = result.drawable,
backgroundColor = itemBackgroundColor,
).getContrastColorIfNeeded(isDarkTheme)
size = pixelsSize,
).getContrastColor(true)
iconBackgroundColor = color
isBackgroundColorDefined = true
}
}
},

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
@ -136,11 +137,13 @@ private inline fun DefaultCurrencyIcon(
crossinline errorIcon: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
var isBackgroundColorDefined by remember { mutableStateOf(false) }
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
val isDarkTheme = isSystemInDarkTheme()
val coroutineScope = rememberCoroutineScope()
val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size48.roundToPx() }
SubcomposeAsyncImage(
modifier = modifier
.background(
@ -149,17 +152,21 @@ private inline fun DefaultCurrencyIcon(
),
model = ImageRequest.Builder(context = LocalContext.current)
.data(iconData)
.size(pixelsSize)
.memoryCacheKey(iconData.toString() + pixelsSize)
.crossfade(enable = true)
.allowHardware(false)
.listener(
onSuccess = { _, result ->
if (isDarkTheme) {
if (!isBackgroundColorDefined && isDarkTheme) {
coroutineScope.launch {
val color = ImageBackgroundContrastChecker(
drawable = result.drawable,
backgroundColor = itemBackgroundColor,
).getContrastColorIfNeeded(isDarkTheme)
size = pixelsSize,
).getContrastColor(isDarkTheme = true)
iconBackgroundColor = color
isBackgroundColorDefined = true
}
}
},