Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-01 12:06:10 +03:00
commit 036e031c1f
1212 changed files with 28739 additions and 8811 deletions

View file

@ -42,7 +42,6 @@ dependencies {
/** Compose */
implementation(deps.compose.constraintLayout)
implementation(deps.compose.foundation)
implementation(deps.compose.material)
implementation(deps.compose.material3)
implementation(deps.compose.paging)
implementation(deps.compose.ui.tooling)

View file

@ -32,9 +32,9 @@ fun AppBarWithBackButton(
TangemTopAppBar(
modifier = modifier,
title = text,
startButton = TopAppBarButtonUM(
startButton = TopAppBarButtonUM.Icon(
iconRes = iconRes ?: R.drawable.ic_back_24,
onIconClicked = onBackClick,
onClicked = onBackClick,
),
containerColor = containerColor,
)

View file

@ -27,14 +27,14 @@ fun AppBarWithBackButtonAndIcon(
title = text,
subtitle = subtitle,
containerColor = backgroundColor,
startButton = TopAppBarButtonUM(
startButton = TopAppBarButtonUM.Icon(
iconRes = backIconRes ?: R.drawable.ic_back_24,
onIconClicked = onBackClick,
onClicked = onBackClick,
),
endButton = if (iconRes != null && onIconClick != null) {
TopAppBarButtonUM(
TopAppBarButtonUM.Icon(
iconRes = iconRes,
onIconClicked = onIconClick,
onClicked = onIconClick,
)
} else {
null

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -20,6 +21,7 @@ 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 com.tangem.core.ui.test.TopAppBarTestTags
/**
* [TangemTopAppBar] height options.
@ -127,6 +129,7 @@ fun TangemTopAppBar(
TopAppBarButton(
button = endButton,
tint = iconTint,
modifier = Modifier.testTag(TopAppBarTestTags.MORE_BUTTON),
)
}
},
@ -172,6 +175,7 @@ fun TangemTopAppBar(
TopAppBarButton(
button = startButton,
tint = iconTint,
modifier = Modifier.testTag(TopAppBarTestTags.CLOSE_BUTTON),
)
}
}
@ -222,6 +226,7 @@ private fun TopAppBarTitle(
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.testTag(TopAppBarTestTags.TITLE),
)
AnimatedVisibility(
@ -296,25 +301,25 @@ private class BasicTopAppBarPMPreviewProvider : PreviewParameterProvider<BasicTo
height = TangemTopAppBarHeight.BOTTOM_SHEET,
),
BasicTopAppBarPM(
startButton = TopAppBarButtonUM(
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_scan_24,
onIconClicked = {},
onClicked = {},
),
endButton = TopAppBarButtonUM(
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_more_vertical_24,
onIconClicked = {},
onClicked = {},
),
),
BasicTopAppBarPM(
startButton = TopAppBarButtonUM(
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_scan_24,
onIconClicked = {},
onClicked = {},
),
),
BasicTopAppBarPM(
endButton = TopAppBarButtonUM(
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_more_vertical_24,
onIconClicked = {},
onClicked = {},
),
height = TangemTopAppBarHeight.BOTTOM_SHEET,
),
@ -322,13 +327,13 @@ private class BasicTopAppBarPMPreviewProvider : PreviewParameterProvider<BasicTo
title = "1234567891011121314151617181920",
subtitle = "12345678910111213141516171819202122232425",
titleAlignment = Alignment.Start,
startButton = TopAppBarButtonUM(
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_scan_24,
onIconClicked = {},
onClicked = {},
),
endButton = TopAppBarButtonUM(
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_more_vertical_24,
onIconClicked = {},
onClicked = {},
),
),
)

View file

@ -1,27 +1,49 @@
package com.tangem.core.ui.components.appbar
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Composable
fun TopAppBarButton(button: TopAppBarButtonUM, tint: Color, modifier: Modifier = Modifier) {
IconButton(
enabled = button.enabled,
modifier = modifier.size(TangemTheme.dimens.size32),
onClick = button.onIconClicked,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = button.iconRes),
tint = tint,
contentDescription = null,
)
when (button) {
is TopAppBarButtonUM.Icon -> {
IconButton(
enabled = button.enabled,
modifier = modifier.size(TangemTheme.dimens.size32),
onClick = button.onClicked,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = button.iconRes),
tint = tint,
contentDescription = null,
)
}
}
is TopAppBarButtonUM.Text -> {
Text(
modifier = modifier
.conditional(button.enabled) {
clickable { button.onClicked() }
}
.padding(4.dp),
text = button.text.resolveReference(),
color = tint,
style = TangemTheme.typography.body1,
)
}
}
}

View file

@ -2,21 +2,39 @@ package com.tangem.core.ui.components.appbar.models
import androidx.annotation.DrawableRes
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
data class TopAppBarButtonUM(
@DrawableRes val iconRes: Int,
val onIconClicked: () -> Unit,
val enabled: Boolean = true,
sealed class TopAppBarButtonUM(
open val onClicked: () -> Unit,
open val enabled: Boolean = true,
) {
data class Icon(
@DrawableRes val iconRes: Int,
override val onClicked: () -> Unit,
override val enabled: Boolean = true,
) : TopAppBarButtonUM(onClicked, enabled)
data class Text(
val text: TextReference,
override val onClicked: () -> Unit,
override val enabled: Boolean = true,
) : TopAppBarButtonUM(onClicked, enabled)
@Suppress("FunctionName")
companion object {
fun Back(onBackClicked: () -> Unit) = Back(true, onBackClicked)
fun Back(enabled: Boolean = true, onBackClicked: () -> Unit) = TopAppBarButtonUM(
fun Back(enabled: Boolean = true, onBackClicked: () -> Unit) = Icon(
iconRes = R.drawable.ic_back_24,
onIconClicked = onBackClicked,
onClicked = onBackClicked,
enabled = enabled,
)
fun Text(text: TextReference, onTextClicked: () -> Unit, enabled: Boolean = true) = Text(
text = text,
onClicked = onTextClicked,
enabled = enabled,
)
}

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -38,6 +39,7 @@ fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) {
)
Text(
modifier = Modifier.weight(1f),
text = model.text.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = when (model.accentType) {
@ -48,6 +50,8 @@ fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) {
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
model.label?.let { Label(it) }
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.core.ui.components.block.model
import androidx.annotation.DrawableRes
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
data class BlockUM(
@ -8,6 +9,7 @@ data class BlockUM(
@DrawableRes val iconRes: Int,
val onClick: () -> Unit,
val accentType: AccentType = AccentType.NONE,
val label: LabelUM? = null,
) {
enum class AccentType {

View file

@ -0,0 +1,99 @@
package com.tangem.core.ui.components.bottomsheets
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.persistentListOf
/**
* Generic options bottom sheet component
*
* @param config Bottom sheet configuration containing OptionsBottomSheetContent
* @param title Title text for the bottom sheet
* @param containerColor Background color of the bottom sheet
*/
@Composable
fun OptionsBottomSheet(
config: TangemBottomSheetConfig,
title: TextReference,
containerColor: androidx.compose.ui.graphics.Color = TangemTheme.colors.background.tertiary,
) {
TangemBottomSheet<OptionsBottomSheetContent>(
config = config,
titleText = title,
containerColor = containerColor,
content = { content ->
OptionsBottomSheetContent(content = content)
},
)
}
@Composable
private fun OptionsBottomSheetContent(content: OptionsBottomSheetContent) {
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
content.options.forEachIndexed { index, option ->
InputRowDefault(
text = option.label,
showDivider = index < content.options.size - 1,
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = content.options.size - 1,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { content.onOptionClick(option.key) },
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun OptionsBottomSheetPreview() {
TangemThemePreview {
OptionsBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = OptionsBottomSheetContent(
options = persistentListOf(
BottomSheetOption(
key = "option1",
label = TextReference.Str("First Option"),
),
BottomSheetOption(
key = "option2",
label = TextReference.Str("Second Option"),
),
BottomSheetOption(
key = "option3",
label = TextReference.Str("Third Option"),
),
),
onOptionClick = {},
),
),
title = TextReference.Str("Select Option"),
)
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.core.ui.components.bottomsheets
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* @param key Unique identifier for the option
* @param label Display text for the option
*/
data class BottomSheetOption(
val key: String,
val label: TextReference,
)
/**
* @param options List of options to display
* @param onOptionClick Callback when an option is clicked, receives the option key
*/
data class OptionsBottomSheetContent(
val options: ImmutableList<BottomSheetOption> = persistentListOf(),
val onOptionClick: (String) -> Unit = {},
) : TangemBottomSheetConfigContent

View file

@ -179,6 +179,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
onBack = onBack,
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
} else {
ModalBottomSheet(
@ -190,6 +191,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
contentWindowInsets = { WindowInsetsZero },
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
}
@ -200,58 +202,62 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
@Composable
private fun TangemModalBottomSheet_Preview() {
TangemThemePreview {
TangemModalBottomSheet<TangemBottomSheetConfigContentPreviewConfig>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContentPreviewConfig(),
),
title = {
TangemModalBottomSheetTitle(
endIconRes = R.drawable.ic_close_24,
onEndClick = {},
)
},
content = {
Column(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.size(56.dp)
.clip(RoundedCornerShape(100))
.background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f))
.padding(12.dp),
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_alert_24),
),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
Box(
Modifier.background(TangemTheme.colors.background.tertiary),
) {
TangemModalBottomSheet<TangemBottomSheetConfigContentPreviewConfig>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContentPreviewConfig(),
),
title = {
TangemModalBottomSheetTitle(
endIconRes = R.drawable.ic_close_24,
onEndClick = {},
)
SpacerH24()
Text(
text = "Unsuported networks",
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH8()
Text(
text = "Tangem does not currently support a required network by React App.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH(48.dp)
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Go it",
onClick = {},
)
}
},
)
},
content = {
Column(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.size(56.dp)
.clip(RoundedCornerShape(100))
.background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f))
.padding(12.dp),
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_alert_24),
),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerH24()
Text(
text = "Unsuported networks",
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH8()
Text(
text = "Tangem does not currently support a required network by React App.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH(48.dp)
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Go it",
onClick = {},
)
}
},
)
}
}
}

View file

@ -32,6 +32,7 @@ import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.core.ui.utils.toPx
/**
* Modal bottom sheet with [content], [footer] and optional [title].
@ -154,6 +155,21 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
val initial = 0
val scrollState = rememberScrollState(initial = initial)
val isKeyboardOpen by rememberIsKeyboardVisible()
val buttonHeight = TangemTheme.dimens.spacing80
val contentBottomPadding = TangemTheme.dimens.spacing80
// Offset calculation for keyboard scroll adjustment:
// 1) Button height (footer)
// 2) Column content bottom padding
// 3) Additional spacing (40dp) for visual comfort when keyboard is open
val scrollOffset = buttonHeight.toPx() + buttonHeight.toPx() + 40.dp.toPx()
LaunchedEffect(isKeyboardOpen) {
if (isKeyboardOpen) {
scrollState.animateScrollTo(scrollState.value + scrollOffset.toInt())
}
}
Column(
modifier = Modifier
.systemBarsPadding()
@ -186,7 +202,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
Column(
modifier = Modifier
.verticalScroll(state = scrollState)
.padding(bottom = TangemTheme.dimens.spacing80),
.padding(bottom = contentBottomPadding),
) {
content(model)
}
@ -199,7 +215,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
Box(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.height(buttonHeight)
.align(Alignment.BottomCenter),
) {
footer(model)
@ -219,6 +235,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
onBack = onBack,
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
} else {
ModalBottomSheet(
@ -230,6 +247,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
contentWindowInsets = { WindowInsetsZero },
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
}

View file

@ -192,6 +192,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
onBack = onBack,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
} else {
ModalBottomSheet(
@ -203,6 +204,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
contentWindowInsets = { WindowInsetsZero },
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
}

View file

@ -57,9 +57,9 @@ private fun Preview_TangemBottomSheetTitle() {
TangemThemePreview {
TangemBottomSheetTitle(
title = "Title",
endButton = TopAppBarButtonUM(
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_information_24,
onIconClicked = {},
onClicked = {},
),
containerColor = TangemTheme.colors.background.secondary,
)

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
@ -21,6 +22,7 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -32,7 +34,9 @@ fun HorizontalActionChips(
contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens.spacing0),
) {
LazyRow(
modifier = modifier.fillMaxWidth(),
modifier = modifier
.fillMaxWidth()
.testTag(TokenDetailsScreenTestTags.HORIZONTAL_ACTION_CHIPS),
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
verticalAlignment = Alignment.CenterVertically,
contentPadding = contentPadding,

View file

@ -20,6 +20,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -33,6 +34,7 @@ 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 com.tangem.core.ui.test.TokenDetailsScreenTestTags
/**
* Rounded action button
@ -98,7 +100,7 @@ fun ActionButton(
),
)
},
modifier = modifier,
modifier = modifier.testTag(TokenDetailsScreenTestTags.ACTION_BUTTON),
color = color,
containerColor = containerColor,
)

View file

@ -25,10 +25,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.DialogTestTags
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.utils.MultipleClickPreventer
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LongMethod")
@Composable
fun TangemButton(
text: String,
@ -51,7 +51,7 @@ fun TangemButton(
Button(
modifier = modifier
.heightIn(min = size.toHeightDp())
.testTag(DialogTestTags.BUTTON),
.testTag(BaseButtonTestTags.BUTTON),
onClick = {
multipleClickPreventer.processEvent { if (!showProgress) onClick() }
},
@ -78,7 +78,8 @@ fun TangemButton(
ResizableText(
modifier = Modifier
.weight(1f, fill = false)
.heightIn(MinButtonContentSize, maxContentSize),
.heightIn(MinButtonContentSize, maxContentSize)
.testTag(BaseButtonTestTags.TEXT),
text = text,
style = textStyle,
color = colors.contentColor(enabled = enabled).value,
@ -92,7 +93,8 @@ fun TangemButton(
Icon(
modifier = Modifier
.buttonContentSize(maxContentSize)
.padding(vertical = 2.dp),
.padding(vertical = 2.dp)
.testTag(BaseButtonTestTags.ICON),
painter = painterResource(id = iconResId),
tint = colors.contentColor(enabled = enabled).value,
contentDescription = null,

View file

@ -3,10 +3,11 @@ package com.tangem.core.ui.components.buttons.small
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -41,20 +42,19 @@ fun TangemIconButton(
background: Color = TangemTheme.colors.button.secondary,
iconTint: Color = TangemTheme.colors.icon.secondary,
) {
IconButton(
onClick = onClick,
Icon(
painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)),
contentDescription = "",
tint = iconTint,
modifier = modifier
.size(24.dp)
.clip(shape)
.background(background)
.size(24.dp),
) {
Icon(
painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)),
contentDescription = "",
tint = iconTint,
modifier = Modifier.size(16.dp),
)
}
.padding(4.dp)
.clickable(
onClick = onClick,
),
)
}
// region Preview

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.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.res.TangemTheme
@ -28,11 +29,16 @@ import com.tangem.core.ui.utils.getGreyScaleColorFilter
* @param shouldDisplayNetwork specifies whether to display network badge
*/
@Composable
fun CurrencyIcon(state: CurrencyIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true) {
fun CurrencyIcon(
state: CurrencyIconState,
modifier: Modifier = Modifier,
shouldDisplayNetwork: Boolean = true,
iconSize: Dp = 36.dp,
) {
BaseContainer(modifier = modifier) {
val iconModifier = Modifier
.align(Alignment.Center)
.size(TangemTheme.dimens.size36)
.size(iconSize)
when (state) {
is CurrencyIconState.Loading -> LoadingIcon(modifier = iconModifier)

View file

@ -0,0 +1,103 @@
package com.tangem.core.ui.components.currency.icon
import androidx.annotation.DrawableRes
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.getTintForTokenIcon
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
import com.tangem.domain.models.currency.CryptoCurrency
object CurrencyIconStateBuilder {
fun build(
cryptoCurrency: CryptoCurrency,
isGrayscale: Boolean = false,
showCustomBadge: Boolean = true,
): CurrencyIconState = when (cryptoCurrency) {
is CryptoCurrency.Coin -> fromCoin(cryptoCurrency, isGrayscale, showCustomBadge)
is CryptoCurrency.Token -> fromToken(cryptoCurrency, isGrayscale, showCustomBadge)
}
private fun createCoinIcon(
url: String? = null,
@DrawableRes fallbackResId: Int = R.drawable.ic_empty_64,
isGrayscale: Boolean = false,
showCustomBadge: Boolean = false,
): CurrencyIconState.CoinIcon = CurrencyIconState.CoinIcon(
url = url,
fallbackResId = fallbackResId,
isGrayscale = isGrayscale,
showCustomBadge = showCustomBadge,
)
private fun createTokenIcon(
url: String? = null,
@DrawableRes topBadgeIconResId: Int? = null,
isGrayscale: Boolean = false,
showCustomBadge: Boolean = false,
fallbackTint: Color = Color.Black,
fallbackBackground: Color = Color.White,
): CurrencyIconState.TokenIcon = CurrencyIconState.TokenIcon(
url = url,
topBadgeIconResId = topBadgeIconResId,
isGrayscale = isGrayscale,
fallbackTint = fallbackTint,
fallbackBackground = fallbackBackground,
showCustomBadge = showCustomBadge,
)
private fun createCustomTokenIcon(
tint: Color,
background: Color,
@DrawableRes topBadgeIconResId: Int,
isGrayscale: Boolean = false,
showCustomBadge: Boolean = true,
): CurrencyIconState.CustomTokenIcon = CurrencyIconState.CustomTokenIcon(
tint = tint,
background = background,
topBadgeIconResId = topBadgeIconResId,
isGrayscale = isGrayscale,
showCustomBadge = showCustomBadge,
)
private fun fromCoin(
coin: CryptoCurrency.Coin,
isGrayscale: Boolean = false,
showCustomBadge: Boolean = true,
): CurrencyIconState.CoinIcon = createCoinIcon(
url = coin.iconUrl,
fallbackResId = coin.networkIconResId,
isGrayscale = isGrayscale || coin.network.isTestnet,
showCustomBadge = coin.isCustom && showCustomBadge,
)
private fun fromToken(
token: CryptoCurrency.Token,
isGrayscale: Boolean = false,
showCustomBadge: Boolean = true,
): CurrencyIconState {
val grayScale = isGrayscale || token.network.isTestnet
val background = token.tryGetBackgroundForTokenIcon(grayScale)
val tint = getTintForTokenIcon(background)
return if (token.isCustom && token.iconUrl == null) {
createCustomTokenIcon(
tint = tint,
background = background,
topBadgeIconResId = token.networkIconResId,
isGrayscale = grayScale,
showCustomBadge = showCustomBadge,
)
} else {
createTokenIcon(
url = token.iconUrl,
topBadgeIconResId = token.networkIconResId,
isGrayscale = grayScale,
fallbackTint = tint,
fallbackBackground = background,
showCustomBadge = token.isCustom && showCustomBadge,
)
}
}
}

View file

@ -5,7 +5,7 @@ import com.tangem.core.ui.extensions.getTintForTokenIcon
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.converter.Converter
/**

View file

@ -12,12 +12,8 @@ import androidx.compose.ui.Alignment.Companion.TopCenter
import androidx.compose.ui.Alignment.Companion.TopStart
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
@ -28,6 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StakingSendScreenTestTags
import com.tangem.core.ui.utils.*
import java.math.BigDecimal
import java.text.DecimalFormat
@ -77,24 +74,10 @@ fun AmountTextField(
) {
val decimalFormat = rememberDecimalFormat()
BoxWithConstraints(modifier = modifier) {
var fontSize = textStyle.fontSize
if (isAutoResize) {
val calculateIntrinsics = @Composable {
val transformedText = visualTransformation.filter(AnnotatedString(value)).text.text
ParagraphIntrinsics(
text = transformedText,
style = textStyle.copy(fontSize = fontSize),
density = LocalDensity.current,
fontFamilyResolver = createFontFamilyResolver(LocalContext.current),
)
}
var intrinsics = calculateIntrinsics()
with(LocalDensity.current) {
while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) {
fontSize *= reduceFactor
intrinsics = calculateIntrinsics()
}
}
val fontSize = if (isAutoResize) {
resizeFont(visualTransformation, value, textStyle, reduceFactor)
} else {
textStyle.fontSize
}
val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color
SimpleTextField(
@ -121,7 +104,9 @@ fun AmountTextField(
singleLine = true,
readOnly = !isEnabled,
visualTransformation = visualTransformation,
modifier = Modifier.background(backgroundColor),
modifier = Modifier
.background(backgroundColor)
.testTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD),
)
}
}

View file

@ -0,0 +1,204 @@
package com.tangem.core.ui.components.fields
import android.annotation.SuppressLint
import androidx.annotation.FloatRange
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.TextUnit
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Simple text field for auto size input.
* Can display aligned placeholder.
*
* @param value initial text
* @param onValueChange callback
* @param isAutoResize is text font auto resize
* @param reduceFactor font resize factor
* @param textStyle text and placeholder styles
* @param textFieldModifier modifier for [SimpleTextField]
* @param boxModifier modifier for [BoxWithConstraints]
* @see [SimpleTextField] for other text field params
*/
@SuppressLint("UnusedBoxWithConstraintsScope")
@Composable
fun AutoSizeTextField(
value: String,
onValueChange: (String) -> Unit,
// region AutoSize
isAutoResize: Boolean = true,
@FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
reduceFactor: Double = 0.9,
// region TextField
textFieldModifier: Modifier = Modifier,
boxModifier: Modifier = Modifier,
placeholder: TextReference? = null,
singleLine: Boolean = isAutoResize,
centered: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
color: Color = TangemTheme.colors.text.primary1,
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,
) {
BoxWithConstraints(modifier = boxModifier) {
val fontSize = if (isAutoResize) {
resizeFont(visualTransformation, value, textStyle, reduceFactor)
} else {
textStyle.fontSize
}
val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color
SimpleTextField(
value = value,
onValueChange = onValueChange,
textStyle = textStyle.copy(
fontSize = fontSize,
textDirection = TextDirection.ContentOrLtr,
),
isValuePasted = isValuePasted,
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
color = textColor,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
placeholder = placeholder,
placeholderColor = placeholderColor,
singleLine = singleLine,
interactionSource = interactionSource,
readOnly = readOnly,
centered = centered,
visualTransformation = visualTransformation,
decorationBox = decorationBox,
modifier = textFieldModifier,
)
}
}
@Composable
internal fun BoxWithConstraintsScope.resizeFont(
visualTransformation: VisualTransformation,
value: String,
textStyle: TextStyle,
reduceFactor: Double,
): TextUnit {
var result = textStyle.fontSize
val calculateIntrinsics = @Composable {
val transformedText = visualTransformation.filter(AnnotatedString(value)).text.text
ParagraphIntrinsics(
text = transformedText,
style = textStyle.copy(fontSize = result),
density = LocalDensity.current,
fontFamilyResolver = createFontFamilyResolver(LocalContext.current),
)
}
var intrinsics = calculateIntrinsics()
with(LocalDensity.current) {
while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) {
result *= reduceFactor
intrinsics = calculateIntrinsics()
}
}
return result
}
// region preview
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun AmountTextFieldPreview(
@PreviewParameter(AutoSizeTextFieldPreviewProvider::class) data: AutoSizeTextFieldPreviewData,
) {
var text by remember { mutableStateOf(data.value) }
TangemThemePreview {
AutoSizeTextField(
textFieldModifier = Modifier.fillMaxWidth(),
value = text,
onValueChange = { text = it },
centered = data.centered,
isAutoResize = data.isAutoResize,
placeholder = data.placeholder,
)
}
}
private class AutoSizeTextFieldPreviewProvider : PreviewParameterProvider<AutoSizeTextFieldPreviewData> {
override val values = sequenceOf(
AutoSizeTextFieldPreviewData(
value = "AutoSizeTextField",
placeholder = stringReference("placeholder"),
isAutoResize = true,
centered = false,
),
AutoSizeTextFieldPreviewData(
value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField",
placeholder = stringReference("placeholder"),
isAutoResize = true,
centered = false,
),
AutoSizeTextFieldPreviewData(
value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField",
placeholder = stringReference("Placeholder"),
isAutoResize = true,
centered = false,
),
AutoSizeTextFieldPreviewData(
value = "",
placeholder = stringReference("Placeholder"),
isAutoResize = true,
centered = false,
),
AutoSizeTextFieldPreviewData(
value = "AutoSizeTextField",
placeholder = stringReference("Placeholder"),
isAutoResize = false,
centered = true,
),
AutoSizeTextFieldPreviewData(
value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField",
placeholder = stringReference("Placeholder"),
isAutoResize = false,
centered = true,
),
AutoSizeTextFieldPreviewData(
value = "",
placeholder = stringReference("Placeholder"),
isAutoResize = false,
centered = true,
),
)
}
private data class AutoSizeTextFieldPreviewData(
val value: String,
val placeholder: TextReference,
val isAutoResize: Boolean,
val centered: Boolean,
)
// endregion

View file

@ -2,6 +2,7 @@ package com.tangem.core.ui.components.fields
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -36,9 +37,9 @@ fun PinTextField(
value: String,
length: Int,
isPasswordVisual: Boolean,
pinTextColor: PinTextColor,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
wrongCode: Boolean = false,
) {
val focusRequester = remember { FocusRequester() }
val textFieldValue = remember(value) {
@ -72,7 +73,7 @@ fun PinTextField(
CellDecoration(
length = length,
isPasswordVisual = isPasswordVisual,
wrongCode = wrongCode,
pinTextColor = pinTextColor,
value = value,
)
},
@ -84,17 +85,25 @@ fun PinTextField(
}
}
@Suppress("MagicNumber")
enum class PinTextColor {
Primary,
WrongCode,
Success,
}
@Suppress("MagicNumber", "LongMethod")
@Composable
private fun CellDecoration(
length: Int,
wrongCode: Boolean,
pinTextColor: PinTextColor,
value: String,
modifier: Modifier = Modifier,
isPasswordVisual: Boolean = false,
) {
val textMeasurer = rememberTextMeasurer()
val width = textMeasurer.measure("0")
val minSize = textMeasurer.measure("0")
val minWidth = maxOf(minSize.size.width.dp + 8.dp, 24.dp + 3.dp) // 24.dp is the minimum width of a pin cell
val minHeight = maxOf(minSize.size.height.dp, 48.dp) // 48.dp is the minimum height of a pin cell
Row(
modifier = modifier,
@ -107,6 +116,18 @@ private fun CellDecoration(
""
}
val color = when (pinTextColor) {
PinTextColor.Primary -> {
if (isPasswordVisual) {
TangemTheme.colors.icon.informative
} else {
TangemTheme.colors.text.primary1
}
}
PinTextColor.WrongCode -> TangemTheme.colors.icon.warning
PinTextColor.Success -> TangemTheme.colors.icon.accent
}
Box(
modifier = Modifier
.background(
@ -119,26 +140,34 @@ private fun CellDecoration(
targetState = char,
transitionSpec = {
(
fadeIn(animationSpec = tween(220, delayMillis = 90)) +
slideInVertically(animationSpec = tween(330, delayMillis = 0))
fadeIn(animationSpec = tween(90, delayMillis = 90)) +
slideInVertically(animationSpec = tween(220, delayMillis = 0))
)
.togetherWith(
fadeOut(animationSpec = tween(90)) + slideOutVertically(tween(220)),
)
},
) { text ->
Text(
modifier = Modifier.sizeIn(minWidth = width.size.width.dp + 8.dp, minHeight = 48.dp),
text = text,
style = TangemTheme.typography.h3,
color = if (wrongCode) {
TangemTheme.colors.text.warning
} else {
TangemTheme.colors.text.primary1
},
textAlign = TextAlign.Center,
lineHeight = 48.sp,
)
if (isPasswordVisual && text.isNotEmpty()) {
Canvas(
Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight),
) {
drawCircle(
color = color,
radius = 4.dp.toPx(),
center = center,
)
}
} else {
Text(
modifier = Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight),
text = text,
style = TangemTheme.typography.h3,
color = color,
textAlign = TextAlign.Center,
lineHeight = 48.sp,
)
}
}
}
}
@ -152,10 +181,18 @@ private fun Preview() {
var text by remember { mutableStateOf("123") }
Column {
PinTextField(
value = text,
onValueChange = { text = it },
isPasswordVisual = true,
pinTextColor = PinTextColor.Success,
length = 6,
)
PinTextField(
value = text,
onValueChange = { text = it },
isPasswordVisual = false,
pinTextColor = PinTextColor.Primary,
length = 6,
)

View file

@ -11,16 +11,20 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@ -35,6 +39,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SelectCountryBottomSheetTestTags
@Composable
fun SearchBar(
@ -45,6 +50,7 @@ fun SearchBar(
) {
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
val interactionSource = remember { MutableInteractionSource() }
BasicTextField(
@ -57,7 +63,9 @@ fun SearchBar(
} else {
state.onActiveChange(false)
}
},
}
.focusRequester(focusRequester)
.testTag(SelectCountryBottomSheetTestTags.SEARCH_BAR),
enabled = enabled,
value = state.query,
onValueChange = state.onQueryChange,
@ -89,6 +97,10 @@ fun SearchBar(
)
},
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
@Suppress("LongParameterList")

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@ -17,6 +18,7 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -39,6 +41,7 @@ fun SimpleTextField(
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
placeholderColor: Color = TangemTheme.colors.text.disabled,
readOnly: Boolean = false,
centered: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
isValuePasted: Boolean = false,
onValuePastedTriggerDismiss: () -> Unit = {},
@ -80,6 +83,8 @@ fun SimpleTextField(
onValuePastedTriggerDismiss()
}
}
var textStyle = textStyle.copy(color = color)
if (centered) textStyle = textStyle.copy(textAlign = TextAlign.Center)
BasicTextField(
value = textFieldValue,
@ -91,7 +96,7 @@ fun SimpleTextField(
if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text)
},
textStyle = textStyle.copy(color = color),
textStyle = textStyle,
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
readOnly = readOnly,
@ -105,6 +110,7 @@ fun SimpleTextField(
value = value,
textStyle = textStyle,
textValue = textValue,
centered = centered,
color = placeholderColor,
)
},
@ -118,10 +124,11 @@ private fun SimpleTextPlaceholder(
placeholder: TextReference?,
value: String,
textStyle: TextStyle,
centered: Boolean,
textValue: @Composable () -> Unit,
color: Color = TangemTheme.colors.text.disabled,
) {
Box {
Box(contentAlignment = if (centered) Alignment.Center else Alignment.TopStart) {
if (value.isBlank() && placeholder != null) {
AnimatedContent(
targetState = placeholder,

View file

@ -16,6 +16,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -26,6 +27,7 @@ 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 com.tangem.core.ui.test.BaseBlockTestTags
/**
* [InputRowDefault](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=86eKp9izWxUvmoCq-4)
@ -64,20 +66,24 @@ fun InputRowDefault(
) {
Column(
modifier = Modifier
.weight(1f),
.weight(1f)
.testTag(BaseBlockTestTags.BLOCK),
) {
title?.let {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = titleColor,
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing8),
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing8)
.testTag(BaseBlockTestTags.BLOCK_TITLE),
)
}
Text(
text = text.resolveReference(),
style = TangemTheme.typography.body2,
color = textColor,
modifier = Modifier.testTag(BaseBlockTestTags.BLOCK_TEXT),
)
}
iconRes?.let {

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment.Companion.CenterEnd
@ -19,6 +20,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.small.TangemIconButton
import com.tangem.core.ui.components.fields.SimpleTextField
@ -69,11 +71,12 @@ fun InputRowRecipient(
showDivider: Boolean = false,
isLoading: Boolean = false,
isValuePasted: Boolean = false,
resolvedAddress: String? = null,
) {
val (titleText, color) = if (isError && error != null) {
error to TangemTheme.colors.text.warning
} else {
title to TangemTheme.colors.text.secondary
title to TangemTheme.colors.text.tertiary
}
DividerContainer(
modifier = modifier,
@ -144,11 +147,15 @@ fun InputRowRecipient(
} else {
TangemTheme.colors.text.primary2
},
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8),
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),
)
}
}
ResolvedAddressRow(
isLoading = isLoading,
resolvedAddress = resolvedAddress,
)
}
}
}
@ -203,6 +210,38 @@ private fun RowScope.InputIcon(isLoading: Boolean, value: String) {
}
}
@Composable
private fun ResolvedAddressRow(isLoading: Boolean, resolvedAddress: String?) {
AnimatedContent(
targetState = if (resolvedAddress.isNullOrBlank() || isLoading) {
ResolvedState.Hide
} else {
ResolvedState.Show(resolvedAddress)
},
label = "Resolved Address",
) { state ->
if (state is ResolvedState.Show) {
Column {
HorizontalDivider(
thickness = 0.5.dp,
modifier = Modifier.padding(top = 12.dp, bottom = 12.dp),
color = TangemTheme.colors.stroke.primary,
)
Text(
text = state.address,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
}
private sealed interface ResolvedState {
data object Hide : ResolvedState
data class Show(val address: String) : ResolvedState
}
//region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@ -224,6 +263,7 @@ private fun InputRowRecipientPreview(
onQrCodeClick = {},
modifier = Modifier.background(TangemTheme.colors.background.primary),
isRedesignEnabled = false,
resolvedAddress = value.resolvedAddress,
)
}
}
@ -232,6 +272,7 @@ private data class InputRowRecipientPreviewData(
val value: String,
val isError: Boolean,
val isLoading: Boolean = false,
val resolvedAddress: String? = null,
)
private class InputRowRecipientPreviewDataProvider : PreviewParameterProvider<InputRowRecipientPreviewData> {
@ -250,6 +291,12 @@ private class InputRowRecipientPreviewDataProvider : PreviewParameterProvider<In
isLoading = true,
isError = true,
),
InputRowRecipientPreviewData(
value = "vitalik.eth",
isLoading = false,
isError = true,
resolvedAddress = "0x391316d97a07027a0702c8A002c8A0C25d8470",
),
)
}
//endregion

View file

@ -0,0 +1,104 @@
package com.tangem.core.ui.components.label
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
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
/**
* Label component
*
* @param state component state
* @param modifier composable modifier
*
* @see <a href="https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=4480-1459&t=2QTpi1G7FeTexTFS-4">Figma</a>
*/
@Composable
fun Label(state: LabelUM, modifier: Modifier = Modifier) {
val backgroundColor by animateColorAsState(
targetValue = when (state.style) {
LabelStyle.ACCENT -> TangemTheme.colors.text.accent.copy(alpha = 0.1f)
LabelStyle.REGULAR -> TangemTheme.colors.control.unchecked
LabelStyle.WARNING -> TangemTheme.colors.text.warning.copy(alpha = 0.1f)
},
)
val textColor by animateColorAsState(
targetValue = when (state.style) {
LabelStyle.ACCENT -> TangemTheme.colors.text.accent
LabelStyle.REGULAR -> TangemTheme.colors.text.secondary
LabelStyle.WARNING -> TangemTheme.colors.text.warning
},
)
AnimatedContent(targetState = state.text) { text ->
Box(
modifier = modifier
.padding(horizontal = 4.dp)
.background(
color = backgroundColor,
shape = TangemTheme.shapes.roundedCorners8,
)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
Text(
text = text.resolveReference(),
style = TangemTheme.typography.caption1,
color = textColor,
)
}
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun LabelPreview() {
TangemThemePreview {
Column(
modifier = Modifier.padding(16.dp),
) {
Label(
state = LabelUM(
text = TextReference.Str("Regular Label"),
style = LabelStyle.REGULAR,
),
)
Spacer(modifier = Modifier.height(8.dp))
Label(
state = LabelUM(
text = TextReference.Str("Accent Label"),
style = LabelStyle.ACCENT,
),
)
Spacer(modifier = Modifier.height(8.dp))
Label(
state = LabelUM(
text = TextReference.Str("Warning Label"),
style = LabelStyle.WARNING,
),
)
}
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.ui.components.label.entity
import com.tangem.core.ui.extensions.TextReference
data class LabelUM(
val text: TextReference,
val style: LabelStyle,
)
enum class LabelStyle {
REGULAR, ACCENT, WARNING,
}

View file

@ -20,7 +20,7 @@ import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.utils.StringsSigns.DASH_SIGN
/**
* Market price block
@ -120,7 +120,7 @@ private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) {
)
}
} else {
Price(price = BigDecimalFormatter.EMPTY_BALANCE_SIGN, modifier = priceModifier)
Price(price = DASH_SIGN, modifier = priceModifier)
}
}
}

View file

@ -20,6 +20,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
@ -35,6 +36,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.NotificationTestTags
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState as NotificationButtonsState
/**
@ -206,6 +208,7 @@ internal fun TextsBlock(
text = titleText,
color = titleColor,
style = TangemTheme.typography.button,
modifier = Modifier.testTag(NotificationTestTags.TITLE),
)
SpacerH(height = TangemTheme.dimens.spacing2)
@ -217,6 +220,7 @@ internal fun TextsBlock(
text = subtitleText,
color = subtitleColor,
style = TangemTheme.typography.caption2,
modifier = Modifier.testTag(NotificationTestTags.TEXT),
)
}
}

View file

@ -23,6 +23,7 @@ import androidx.constraintlayout.compose.Visibility
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.audits.AuditLabel
import com.tangem.core.ui.components.audits.AuditLabelUM
import com.tangem.core.ui.components.badge.Badge
@ -50,8 +51,6 @@ private const val DISABLED_ICON_ALPHA = 0.4f
fun ProviderChooseCrypto(providerChooseUM: ProviderChooseUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
ConstraintLayout(
modifier = modifier
.background(TangemTheme.colors.background.action)
.clip(RoundedCornerShape(14.dp))
.selectedBorder(isSelected = providerChooseUM.isSelected)
.clickable(
enabled = !providerChooseUM.hasError(),
@ -133,13 +132,23 @@ private fun IconContent(iconUrl: String, modifier: Modifier = Modifier) {
SubcomposeAsyncImage(
modifier = modifier
.size(40.dp)
.clip(RoundedCornerShape(8.dp))
.background(TangemColorPalette.Light1),
.clip(RoundedCornerShape(8.dp)),
model = ImageRequest.Builder(context = LocalContext.current)
.data(iconUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
loading = {
RectangleShimmer(radius = 8.dp)
},
error = {
Box(
modifier = Modifier.background(
color = TangemColorPalette.Light1,
shape = RoundedCornerShape(8.dp),
),
)
},
contentDescription = null,
)
}

View file

@ -14,6 +14,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
@ -23,6 +24,7 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StakingDetailsScreenTestTags
@Suppress("LongParameterList")
@Composable
@ -54,7 +56,8 @@ fun RoundableCornersRow(
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
),
)
.testTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
@ -63,6 +66,7 @@ fun RoundableCornersRow(
color = startTextColor,
maxLines = 1,
style = startTextStyle,
modifier = Modifier.testTag(StakingDetailsScreenTestTags.PARAMETER_NAME),
)
if (iconResId != null && iconClick != null) {
Icon(
@ -85,6 +89,7 @@ fun RoundableCornersRow(
color = endTextColor,
maxLines = 1,
style = endTextStyle,
modifier = Modifier.testTag(StakingDetailsScreenTestTags.PARAMETER_VALUE),
)
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.core.ui.components.rows
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -13,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
@ -22,14 +22,15 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
import com.tangem.utils.StringsSigns
@Composable
fun SelectorRowItem(
@StringRes titleRes: Int,
title: TextReference,
@DrawableRes iconRes: Int,
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(TangemTheme.dimens.spacing12),
@ -68,7 +69,8 @@ fun SelectorRowItem(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues),
.padding(paddingValues)
.testTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
@ -77,7 +79,7 @@ fun SelectorRowItem(
contentDescription = null,
)
Text(
text = stringResourceSafe(titleRes),
text = title.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),
@ -148,7 +150,7 @@ private fun RowScope.SelectorValueContent(
private fun SelectorRowItemPreview() {
TangemThemePreview {
SelectorRowItem(
titleRes = R.string.common_fee_selector_option_slow,
title = resourceReference(R.string.common_fee_selector_option_slow),
iconRes = R.drawable.ic_tortoise_24,
preDot = TextReference.Str("1000 ETH"),
postDot = TextReference.Str("1000 $"),

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -28,6 +29,7 @@ import com.tangem.core.ui.components.stories.model.StoryConfig
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SwapStoriesScreenTestTags
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -101,7 +103,8 @@ inline fun <reified T : StoryConfig> StoriesContainer(
interactionSource = remember { MutableInteractionSource() },
indication = LocalIndication.current,
onClick = { config.onClose(watchedCounter) },
),
)
.testTag(SwapStoriesScreenTestTags.CLOSE_BUTTON),
)
}
}

View file

@ -94,6 +94,7 @@ fun getActiveIconRes(blockchainId: String): Int {
"zklink", "zklink/test" -> R.drawable.img_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22
"pepecoin", "pepecoin/test" -> R.drawable.img_pepecoin_22
"hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22
else -> R.drawable.ic_alert_24
}
}
@ -186,6 +187,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"zklink", "zklink/test" -> R.drawable.img_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22
"pepecoin-network", "pepecoin-network/test" -> R.drawable.img_pepecoin_22
"hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22
else -> R.drawable.ic_alert_24
}
}
@ -281,6 +283,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"zklink", "zklink/test" -> R.drawable.ic_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.ic_vanar_22
"pepecoin", "pepecoin/test" -> R.drawable.ic_pepecoin_22
"hyperliquid", "hyperliquid/test" -> R.drawable.ic_hyperliquid_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -1,49 +0,0 @@
package com.tangem.core.ui.extensions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import timber.log.Timber
/**
* The ViewModel is scoped to the parent route Navigation graph
* and is provided using the Hilt-generated ViewModel factory
*
* ```
* val navController = rememberNavController()
*
* navigation(
* route = "parent",
* startDestination = "parent/1"
* ) {
* composable("route/1") { entry ->
* val viewModel = entry.parentHiltViewModel(navController)
* }
* composable("route/2") { entry ->
* val viewModel = entry.parentHiltViewModel(navController)
* }
* composable("route/3") { entry ->
* val viewModel = entry.parentHiltViewModel(navController)
* }
* }
* ```
*
* @param navController NavController within the common NavGraph
* @throws Exception if there is no parent route
*/
@Composable
inline fun <reified T : ViewModel> NavBackStackEntry.parentHiltViewModel(navController: NavController): T {
val viewModelStoreOwner = remember(this) {
try {
navController.getBackStackEntry(this.destination.parent!!.id)
} catch (e: Exception) {
Timber.tag("scopedViewModel").e(e, "There is no parent route'")
throw e
}
}
return hiltViewModel<T>(viewModelStoreOwner)
}

View file

@ -1,43 +0,0 @@
package com.tangem.core.ui.extensions
import android.R
import android.content.Context
import android.graphics.Color.*
import android.view.WindowManager
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import kotlin.math.sqrt
@Deprecated("Use only in legacy fragments")
fun Fragment.setStatusBarColor(@ColorRes colorResId: Int) {
with(requireActivity().window) {
clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
statusBarColor = ContextCompat.getColor(requireContext(), colorResId)
val view = view ?: return
val windowInsetsController = WindowCompat.getInsetsController(this, view)
windowInsetsController.isAppearanceLightStatusBars = luminance(requireContext(), colorResId)
}
}
// TODO replace by android.graphics.luminance() after bump min API to 24
@Suppress("MagicNumber")
fun luminance(context: Context, @ColorRes colorRes: Int): Boolean {
val color = context.resources.getColor(colorRes, null)
if (R.color.transparent == color) return true
var rtnValue = false
val rgb = intArrayOf(red(color), green(color), blue(color))
val brightness = sqrt(
rgb[0] * rgb[0] * .241 +
rgb[1] * rgb[1] * .691 +
rgb[2] * rgb[2] * .068,
).toInt()
// color is light
if (brightness >= 200) {
rtnValue = true
}
return rtnValue
}

View file

@ -4,7 +4,6 @@ import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -72,26 +71,24 @@ fun Modifier.conditionalCompose(
fun Modifier.selectedBorder(
isSelected: Boolean,
width: Dp = 2.5.dp,
color: Color = TangemTheme.colors.text.accent.copy(alpha = 0.1f),
color: Color = TangemTheme.colors.text.accent,
radius: Dp = 16.dp,
) = conditionalCompose(
condition = isSelected,
modifier = {
border(
outsetBorder(
width = width,
color = color,
shape = RoundedCornerShape(radius),
color = color.copy(alpha = 0.15f),
shape = RoundedCornerShape(radius + 2.dp),
)
.padding(width)
.border(
width = 1.dp,
color = TangemTheme.colors.text.accent,
shape = RoundedCornerShape(radius - 2.dp),
color = color,
shape = RoundedCornerShape(radius),
)
.clip(RoundedCornerShape(radius - 2.dp))
.clip(RoundedCornerShape(radius))
},
otherModifier = {
padding(width)
.clip(RoundedCornerShape(radius - 2.dp))
clip(RoundedCornerShape(radius))
},
)

View file

@ -121,7 +121,10 @@ fun BigDecimalFiatFormat.price(): BigDecimalFormat = BigDecimalFormat { value ->
private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
private fun getFiatPriceAmountWithScale(value: BigDecimal): Pair<BigDecimal, Int> {
/**
* Returns amount with correct scale
*/
fun getFiatPriceAmountWithScale(value: BigDecimal): Pair<BigDecimal, Int> {
return if (value < BigDecimal.ONE) {
val leadingZeroes = value.scale() - value.precision()
val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES

View file

@ -16,6 +16,7 @@ class TangemColors internal constructor(
control: Control,
stroke: Stroke,
field: Field,
overlay: Overlay,
) {
var text by mutableStateOf(text)
private set
@ -31,6 +32,7 @@ class TangemColors internal constructor(
private set
var field by mutableStateOf(field)
private set
var overlay by mutableStateOf(overlay)
@Stable
class Text internal constructor(
@ -220,6 +222,22 @@ class TangemColors internal constructor(
}
}
@Stable
class Overlay internal constructor(
primary: Color,
secondary: Color,
) {
var primary by mutableStateOf(primary)
private set
var secondary by mutableStateOf(secondary)
private set
fun update(other: Overlay) {
primary = other.primary
secondary = other.secondary
}
}
fun update(other: TangemColors) {
text.update(other.text)
icon.update(other.icon)
@ -228,5 +246,6 @@ class TangemColors internal constructor(
control.update(other.control)
stroke.update(other.stroke)
field.update(other.field)
overlay.update(other.overlay)
}
}

View file

@ -4,9 +4,9 @@ import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material.Colors
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
@ -90,7 +90,7 @@ fun TangemTheme(
val rootBackgroundColor = rememberedColors.background.secondary
MaterialTheme(
colors = materialThemeColors(colors = themeColors, isDark = isDark),
colorScheme = tangemColorScheme(colors = themeColors),
) {
CompositionLocalProvider(
LocalTangemColors provides rememberedColors,
@ -143,21 +143,51 @@ object TangemTheme {
@Stable
@Composable
private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors {
return Colors(
private fun tangemColorScheme(colors: TangemColors): ColorScheme {
return ColorScheme(
primary = colors.background.primary,
primaryVariant = colors.background.secondary,
secondary = colors.button.primary,
secondaryVariant = colors.text.accent,
background = colors.background.primary,
surface = colors.background.secondary,
error = colors.text.warning,
onPrimary = colors.text.primary1,
primaryContainer = colors.background.secondary,
onPrimaryContainer = colors.background.action,
inversePrimary = colors.background.action,
secondary = colors.button.primary,
onSecondary = colors.text.primary1,
secondaryContainer = colors.background.secondary,
onSecondaryContainer = colors.text.primary1,
tertiary = colors.background.tertiary,
onTertiary = colors.text.tertiary,
tertiaryContainer = colors.background.tertiary,
onTertiaryContainer = colors.text.tertiary,
background = colors.background.primary,
onBackground = colors.text.primary1,
surface = colors.background.secondary,
surfaceVariant = colors.background.tertiary,
onSurface = colors.text.primary1,
onSurfaceVariant = colors.text.secondary,
surfaceTint = colors.background.tertiary,
inverseSurface = colors.button.disabled,
inverseOnSurface = colors.button.primary,
surfaceBright = colors.background.secondary,
surfaceDim = colors.background.tertiary,
surfaceContainer = colors.background.tertiary,
surfaceContainerHigh = colors.background.tertiary,
surfaceContainerHighest = colors.background.tertiary,
surfaceContainerLow = colors.background.tertiary,
surfaceContainerLowest = colors.background.tertiary,
error = colors.text.warning,
errorContainer = colors.background.tertiary,
onErrorContainer = colors.text.primary2,
onError = colors.text.primary2,
isLight = !isDark,
outline = colors.stroke.primary,
outlineVariant = colors.stroke.secondary,
scrim = colors.stroke.transparency,
)
}
@ -208,6 +238,10 @@ private fun lightThemeColors(): TangemColors {
primary = TangemColorPalette.Light1,
focused = TangemColorPalette.Light2,
),
overlay = TangemColors.Overlay(
primary = TangemColorPalette.Black.copy(alpha = 0.4f),
secondary = TangemColorPalette.Black.copy(alpha = 0.7f),
),
)
}
@ -258,6 +292,10 @@ private fun darkThemeColors(): TangemColors {
primary = TangemColorPalette.Dark5,
focused = TangemColorPalette.Dark4,
),
overlay = TangemColors.Overlay(
primary = TangemColorPalette.Black.copy(alpha = 0.4f),
secondary = TangemColorPalette.Black.copy(alpha = 0.7f),
),
)
}

View file

@ -1,72 +0,0 @@
package com.tangem.core.ui.screen
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.FloatRange
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
/**
* An abstract base class for bottom sheet dialogs that use Compose for UI rendering.
* Extends [BottomSheetDialogFragment] and implements [ComposeScreen] interface.
*/
abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), ComposeScreen {
/**
* The initial state of the bottom sheet. Default is [BottomSheetBehavior.STATE_EXPANDED].
*/
open val initialBottomSheetState = BottomSheetBehavior.STATE_EXPANDED
/**
* The fraction of the screen height that the bottom sheet should take when expanded.
* Default is `null`, indicating that the height will be determined by the content.
*/
@FloatRange(from = 0.0, to = 1.0)
open val expandedHeightFraction: Float? = null
override val screenModifier: Modifier
@Composable
@ReadOnlyComposable
get() = Modifier
.fillMaxWidth()
.let {
if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it
}
.background(
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.bottomSheet,
)
override fun getTheme(): Int = R.style.AppTheme_TransparentBottomSheetDialog
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return createComposeView(
context = inflater.context,
activity = requireActivity(),
overrideSystemBarColors = false,
)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
(dialog as BottomSheetDialog).behavior.apply {
state = initialBottomSheetState
skipCollapsed = true
}
return dialog
}
}

View file

@ -1,50 +0,0 @@
package com.tangem.core.ui.screen
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.core.ui.R
/**
* An abstract base class for fragments that use Compose for UI rendering.
* Extends [Fragment] and implements [ComposeScreen] interface.
*/
abstract class ComposeFragment : Fragment(), ComposeScreen {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val isTransitionsInflated = TransitionInflater.from(requireContext()).inflateTransitions()
return createComposeView(inflater.context, requireActivity()).also {
it.isTransitionGroup = isTransitionsInflated
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
/*
* We need to manually dispatch configuration changes to the Compose view.
*
* `android:configChanges="uiMode"` is set in the manifest.
* */
view?.dispatchConfigurationChanged(newConfig)
}
/**
* Inflates transitions for the fragment. Override this method to customize
* enter and exit transitions for the fragment.
*
* @return `true` if transitions were inflated; `false` otherwise.
*/
protected open fun TransitionInflater.inflateTransitions(): Boolean {
enterTransition = inflateTransition(R.transition.fade)
exitTransition = inflateTransition(R.transition.fade)
return true
}
}

View file

@ -0,0 +1,7 @@
package com.tangem.core.ui.test
object BaseBlockTestTags {
const val BLOCK = "BASE_BLOCK"
const val BLOCK_TITLE = "BASE_BLOCK_TITLE"
const val BLOCK_TEXT = "BASE_BLOCK_REWARDS_TEXT"
}

View file

@ -0,0 +1,7 @@
package com.tangem.core.ui.test
object BaseButtonTestTags {
const val BUTTON = "BASE_BUTTON"
const val ICON = "BASE_BUTTON_ICON"
const val TEXT = "BASE_BUTTON_TEXT"
}

View file

@ -0,0 +1,16 @@
package com.tangem.core.ui.test
object BuyTokenDetailsScreenTestTags {
const val EXPAND_FIAT_LIST_BUTTON = "BUY_TOKEN_DETAILS_SCREEN_EXPAND_FIAT_LIST_BUTTON"
const val FIAT_CURRENCY_ICON = "BUY_TOKEN_DETAILS_SCREEN_FIAT_CURRENCY_ICON"
const val FIAT_AMOUNT_TEXT_FIELD = "BUY_TOKEN_DETAILS_SCREEN_FIAT_AMOUNT_TEXT_FIELD"
const val TOKEN_AMOUNT = "BUY_TOKEN_DETAILS_SCREEN_TOKEN_AMOUNT"
const val PROVIDER_LOADING_TITLE = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_LOADING_TITLE"
const val PROVIDER_LOADING_TEXT = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_LOADING_TITLE"
const val PROVIDER_TITLE = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_TITLE"
const val PROVIDER_TEXT = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_TEXT"
const val TOS_BLOCK = "BUY_TOKEN_DETAILS_SCREEN_TOS_BLOCK"
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object BuyTokenFiatListTestTags {
const val LAZY_LIST = "BUY_TOKEN_FIAT_LIST_LAZY_LIST"
const val LAZY_LIST_ITEM = "BUY_TOKEN_FIAT_LIST_LAZY_LIST_ITEM"
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object BuyTokenScreenTestTags {
const val LAZY_LIST = "BUY_TOKEN_SCREEN_LAZY_LIST"
const val LAZY_LIST_ITEM = "BUY_TOKEN_SCREEN_LAZY_LIST_ITEM"
}

View file

@ -2,5 +2,4 @@ package com.tangem.core.ui.test
object DialogTestTags {
const val DIALOG_CONTAINER = "DIALOG_CONTAINER"
const val BUTTON = "DIALOG_BUTTON"
}

View file

@ -3,4 +3,5 @@ package com.tangem.core.ui.test
object DisclaimerScreenTestTags {
const val SCREEN_CONTAINER = "DISCLAIMER_SCREEN_CONTAINER"
const val ACCEPT_BUTTON = "DISCLAIMER_SCREEN_ACCEPT_BUTTON"
const val WEB_VIEW = "DISCLAIMER_SCREEN_WEB_VIEW"
}

View file

@ -8,4 +8,5 @@ object MainScreenTestTags {
const val WALLET_BALANCE = "MAIN_SCREEN_WALLET_BALANCE"
const val WALLET_LIST_ITEM = "MAIN_SCREEN_WALLET_LIST_ITEM"
const val ORGANIZE_TOKENS_BUTTON = "MAIN_SCREEN_ORGANIZE_TOKENS_BUTTON"
const val MULTI_CURRENCY_ACTION_BUTTON = "MAIN_SCREEN_MULTI_CURRENCY_ACTION_BUTTON"
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object NotificationTestTags {
const val TITLE = "NOTIFICATION_TITLE"
const val TEXT = "NOTIFICATION_TEXT"
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.ui.test
object ReferralProgramScreenTestTags {
const val IMAGE = "REFERRAL_PROGRAM_SCREEN_IMAGE"
const val CONDITION_BLOCK = "REFERRAL_PROGRAM_SCREEN_CONDITION_BLOCK"
const val INFO_FOR_YOU_TEXT = "REFERRAL_PROGRAM_SCREEN_INFO_FOR_YOU_TEXT"
const val INFO_FOR_YOUR_FRIEND_TEXT = "REFERRAL_PROGRAM_INFO_FOR_YOUR_FRIEND_TEXT"
}

View file

@ -0,0 +1,5 @@
package com.tangem.core.ui.test
object ResidenceSettingsScreenTestTags {
const val COUNTRY_NAME = "RESIDENCE_SETTINGS_SCREEN_COUNTRY_NAME"
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.ui.test
object SelectCountryBottomSheetTestTags {
const val LAZY_LIST = "SELECT_COUNTRY_BOTTOM_SHEET_LAZY_LIST"
const val COUNTRY_ITEM = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_ITEM"
const val UNAVAILABLE_COUNTRY_ITEM = "SELECT_COUNTRY_BOTTOM_SHEET_UNAVAILABLE_COUNTRY_ITEM"
const val SEARCH_BAR = "SELECT_COUNTRY_BOTTOM_SHEET_SEARCH_BAR"
const val COUNTRY_ICON = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_ICON"
const val UNAVAILABLE_COUNTRY_ICON = "SELECT_COUNTRY_BOTTOM_SHEET_UNAVAILABLE_COUNTRY_ICON"
const val COUNTRY_NAME = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_NAME"
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object SelectNetworkFeeBottomSheetTestTags {
const val READ_MORE_TEXT = "SELECT_NETWORK_FEE_READ_MORE_TEXT"
const val SELECTOR_ITEM = "SELECT_NETWORK_FEE_SELECTOR_ITEM"
}

View file

@ -0,0 +1,7 @@
package com.tangem.core.ui.test
object SelectPaymentMethodBottomSheetTestTags {
const val LAZY_LIST = "SELECT_PAYMENT_METHOD_LAZY_LIST"
const val PAYMENT_METHOD_ICON = "PAYMENT_METHOD_NAME"
}

View file

@ -0,0 +1,18 @@
package com.tangem.core.ui.test
object SelectProviderBottomSheetTestTags {
const val PAYMENT_METHOD_ICON = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_ICON"
const val PAYMENT_METHOD_TITLE = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_TITLE"
const val PAYMENT_METHOD_NAME = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_NAME"
const val PAYMENT_METHOD_EXPAND_BUTTON = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_EXPAND_BUTTON"
const val TOKEN_AMOUNT = "SELECT_PROVIDER_BOTTOM_SHEET_TOKEN_AMOUNT"
const val AVAILABLE_PROVIDER_NAME = "SELECT_PROVIDER_BOTTOM_SHEET_AVAILABLE_PROVIDER_NAME"
const val AVAILABLE_PROVIDER_ITEM = "SELECT_PROVIDER_BOTTOM_SHEET_AVAILABLE_PROVIDER_ITEM"
const val UNAVAILABLE_PROVIDER_ITEM = "SELECT_PROVIDER_BOTTOM_SHEET_UNAVAILABLE_PROVIDER_ITEM"
const val UNAVAILABLE_PROVIDER_NAME = "SELECT_PROVIDER_BOTTOM_SHEET_UNAVAILABLE_PROVIDER_NAME"
const val UNAVAILABLE_PROVIDER_SUBTITLE = "SELECT_PROVIDER_BOTTOM_SHEET_UNAVAILABLE_PROVIDER_SUBTITLE"
const val MORE_PROVIDERS_ICON = "SELECT_PROVIDER_BOTTOM_SHEET_MORE_PROVIDERS_ICON"
const val MORE_PROVIDERS_TEXT = "SELECT_PROVIDER_BOTTOM_SHEET_MORE_PROVIDERS_TEXT"
const val BEST_RATE_LABEL = "SELECT_PROVIDER_BOTTOM_SHEET_BEST_RATE_LABEL"
}

View file

@ -0,0 +1,15 @@
package com.tangem.core.ui.test
object StakingDetailsScreenTestTags {
const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER"
const val BANNER_IMAGE = "TOKEN_DETAILS_SCREEN_BANNER_IMAGE"
const val BANNER_TEXT = "TOKEN_DETAILS_SCREEN_BANNER_TEXT"
const val PARAMETER_BLOCK = "STAKING_DETAILS_PARAMETER_BLOCK"
const val PARAMETER_NAME = "STAKING_DETAILS_PARAMETER_NAME"
const val PARAMETER_VALUE = "STAKING_DETAILS_PARAMETER_VALUE"
const val TOS_TEXT = "STAKING_DETAILS_TOS_TEXT"
const val ACTIVE_STAKING_BLOCK = "STAKING_DETAILS_ACTIVE_STAKING_BLOCK"
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.test
object StakingSendDetailsScreenTestTags {
const val PRIMARY_AMOUNT = "STAKING_SEND_DETAILS_SCREEN_PRIMARY_AMOUNT"
const val SECONDARY_AMOUNT = "TAKING_SEND_DETAILS_SCREEN_SECONDARY_AMOUNT"
const val VALIDATOR_BLOCK = "TAKING_SEND_DETAILS_SCREEN_VALIDATOR_BLOCK"
const val NETWORK_FEE_BLOCK = "TAKING_SEND_DETAILS_SCREEN_NETWORK_FEE_BLOCK"
}

View file

@ -0,0 +1,16 @@
package com.tangem.core.ui.test
object StakingSendScreenTestTags {
const val SCREEN_CONTAINER = "STAKING_SEND_SCREEN_CONTAINER"
const val AMOUNT_CONTAINER_TITLE = "STAKING_SEND_SCREEN_AMOUNT_CONTAINER_TITLE"
const val AMOUNT_CONTAINER_TEXT = "STAKING_SEND_SCREEN_AMOUNT_CONTAINER_TEXT"
const val INPUT_TEXT_FIELD = "STAKING_SEND_SCREEN_INPUT_TEXT_FIELD"
const val SECONDARY_AMOUNT = "STAKING_SEND_SCREEN_SECONDARY_AMOUNT"
const val CURRENCY_BUTTON = "STAKING_SEND_SCREEN_CURRENCY_BUTTON"
const val FIAT_ICON = "STAKING_SEND_SCREEN_FIAT_ICON"
const val CURRENCY_ICON = "STAKING_SEND_SCREEN_CURRENCY_ICON"
const val MAX_BUTTON = "STAKING_SEND_SCREEN_MAX_BUTTON"
const val PREVIOUS_BUTTON = "STAKING_SEND_SCREEN_PREVIOUS_BUTTON"
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object SwapStoriesScreenTestTags {
const val SCREEN_CONTAINER = "SWAP_STORIES_SCREEN_CONTAINER"
const val CLOSE_BUTTON = "SWAP_STORIES_SCREEN_CLOSE_BUTTON"
}

View file

@ -0,0 +1,14 @@
package com.tangem.core.ui.test
object SwapTokenScreenTestTags {
const val SWAP_BLOCK_HEADER = "SWAP_TOKEN_SCREEN_SWAP_BLOCK"
const val BALANCE = "SWAP_TOKEN_SCREEN_BALANCE"
const val SWAP_TEXT_FIELD = "SWAP_TOKEN_SCREEN_SWAP_TEXT_FIELD"
const val RECEIVE_TEXT_FIELD = "SWAP_TOKEN_SCREEN_RECEIVE_TEXT_FIELD"
const val RECEIVE_AMOUNT_SHIMMER = "SWAP_TOKEN_SCREEN_RECEIVE_AMOUNT_SHIMMER"
const val PROVIDERS_BLOCK = "SWAP_TOKEN_SCREEN_PROVIDERS_BLOCK"
const val SWAP_BUTTON = "SWAP_TOKEN_SCREEN_SWAP_BUTTON"
const val TOKEN = "SWAP_TOKEN_SCREEN_TOKEN"
const val TOKEN_NAME = "SWAP_TOKEN_SCREEN_TOKEN_NAME"
const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON"
}

View file

@ -2,4 +2,19 @@ package com.tangem.core.ui.test
object TokenDetailsScreenTestTags {
const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER"
const val TOKEN_TITLE = "TOKEN_DETAILS_SCREEN_TOKEN_TITLE"
const val ACTION_BUTTON = "TOKEN_DETAILS_SCREEN_ACTION_BUTTON"
const val HORIZONTAL_ACTION_CHIPS = "TOKEN_DETAILS_SCREEN_HORIZONTAL_ACTION_CHIPS"
const val STAKING_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_BLOCK"
const val STAKING_AVAILABLE_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_AVAILABLE_BLOCK"
const val STAKING_CURRENCY_ICON = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_CURRENCY_ICON"
const val STAKING_SERVICE_TITLE = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_SERVICE_TITLE"
const val STAKING_SERVICE_TEXT = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_SERVICE_TEXT"
const val STAKING_FIAT_AMOUNT = "TOKEN_DETAILS_SCREEN_STAKING_FIAT_AMOUNT"
const val STAKING_DOT = "TOKEN_DETAILS_SCREEN_STAKING_DOT"
const val STAKING_TOKEN_AMOUNT = "TOKEN_DETAILS_SCREEN_STAKING_TOKEN_AMOUNT"
const val STAKING_REWARD_VALUE = "TOKEN_DETAILS_SCREEN_STAKING_REWARD_VALUE"
const val STAKING_CHEVRON_ICON = "TOKEN_DETAILS_SCREEN_STAKING_CHEVRON_ICON"
}

View file

@ -0,0 +1,7 @@
package com.tangem.core.ui.test
object TopAppBarTestTags {
const val TITLE = "TOP_APP_BAR_TITLE"
const val MORE_BUTTON = "TOP_APP_BAR_MORE_BUTTON"
const val CLOSE_BUTTON = "TOP_APP_BAR_CLOSE_BUTTON"
}

View file

@ -0,0 +1,54 @@
package com.tangem.core.ui.utils
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
/**
[REDACTED_AUTHOR]
*/
typealias AnimatedValue = Pair<Float, Float>
@Composable
fun AnimatedValue.toAnimatable(
isPaused: Boolean,
duration: Int,
easing: Easing = LinearEasing,
): Animatable<Float, AnimationVector1D> {
return animatable(
values = this,
isPaused = isPaused,
duration = duration,
easing = easing,
)
}
@Composable
fun animatable(
values: AnimatedValue,
duration: Int,
isPaused: Boolean = false,
easing: Easing = LinearEasing,
): Animatable<Float, AnimationVector1D> {
val animatable = remember { Animatable(values.first) }
LaunchedEffect(isPaused) {
if (isPaused) {
animatable.stop()
} else {
animatable.animateTo(
targetValue = values.second,
animationSpec = tween(
durationMillis = duration,
easing = easing,
),
)
}
}
return animatable
}

View file

@ -1,147 +0,0 @@
package com.tangem.core.ui.utils
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.StringsSigns.LOWER_SIGN
import com.tangem.utils.StringsSigns.TILDE_SIGN
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.NumberFormat
import java.util.Currency
import java.util.Locale
@Suppress("LargeClass")
@Deprecated("Use BigDecimal.format")
object BigDecimalFormatter {
const val EMPTY_BALANCE_SIGN = DASH_SIGN
private const val CAN_BE_LOWER_SIGN = LOWER_SIGN
private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01")
private const val FIAT_MARKET_DEFAULT_DIGITS = 2
private const val FIAT_MARKET_EXTENDED_DIGITS = 6
private const val FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES = 4
private val usdCurrency = Currency.getInstance("USD")
@Deprecated("Use BigDecimal.format")
fun formatFiatAmount(
fiatAmount: BigDecimal?,
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
decimals: Int = FIAT_MARKET_DEFAULT_DIGITS,
locale: Locale = Locale.getDefault(),
withApproximateSign: Boolean = false,
): String {
if (fiatAmount == null) return EMPTY_BALANCE_SIGN
val formatterCurrency = getCurrency(fiatCurrencyCode)
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
currency = formatterCurrency
maximumFractionDigits = decimals
minimumFractionDigits = decimals
roundingMode = RoundingMode.HALF_UP
}
return if (fiatAmount.checkFiatThreshold()) {
buildString {
append(CAN_BE_LOWER_SIGN)
append(
formatter.format(FIAT_FORMAT_THRESHOLD)
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol),
)
}
} else {
val formattedAmount = formatter.format(fiatAmount)
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
if (withApproximateSign) {
buildString {
append(TILDE_SIGN)
append(formattedAmount)
}
} else {
formattedAmount
}
}
}
@Deprecated("Use BigDecimal.format")
fun formatFiatAmountUncapped(
fiatAmount: BigDecimal?,
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
locale: Locale = Locale.getDefault(),
): String {
if (fiatAmount == null) return EMPTY_BALANCE_SIGN
val formatterCurrency = getCurrency(fiatCurrencyCode)
val digits = if (fiatAmount.checkFiatThreshold()) {
FIAT_MARKET_EXTENDED_DIGITS
} else {
FIAT_MARKET_DEFAULT_DIGITS
}
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
currency = formatterCurrency
maximumFractionDigits = digits
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
roundingMode = RoundingMode.HALF_UP
}
return formatter.format(fiatAmount)
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
}
@Deprecated("Use BigDecimal.format")
fun formatFiatPriceUncapped(
fiatAmount: BigDecimal?,
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
locale: Locale = Locale.getDefault(),
): String {
if (fiatAmount == null) return EMPTY_BALANCE_SIGN
val formatterCurrency = getCurrency(fiatCurrencyCode)
val (formattedAmount, finalScale) = getFiatPriceUncappedWithScale(value = fiatAmount)
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
currency = formatterCurrency
maximumFractionDigits = finalScale
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
roundingMode = RoundingMode.HALF_UP
}
return formatter.format(formattedAmount)
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
}
@Deprecated("Use BigDecimal.format")
fun getFiatPriceUncappedWithScale(value: BigDecimal): Pair<BigDecimal, Int> {
return if (value < BigDecimal.ONE) {
val leadingZeroes = value.scale() - value.precision()
val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES
val amount = value
.setScale(scale, RoundingMode.HALF_UP)
.stripTrailingZeros()
amount to amount.scale()
} else {
value to FIAT_MARKET_DEFAULT_DIGITS
}
}
private fun getCurrency(code: String): Currency {
return runCatching { Currency.getInstance(code) }
.getOrElse { e ->
// Currency code is not valid ISO 4217 code
if (e is IllegalArgumentException) {
usdCurrency
} else {
throw e
}
}
}
private fun BigDecimal.checkFiatThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
}

View file

@ -1,9 +1,15 @@
package com.tangem.core.ui.utils
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
@Stable
@Composable
@ -15,4 +21,16 @@ fun convertPxToDp(px: Float): Dp = convertPxToDp(px, density = LocalDensity.curr
fun Dp.toPx(density: Float): Float = this.value * density
fun convertPxToDp(px: Float, density: Float): Dp = Dp(value = px / density)
fun convertPxToDp(px: Float, density: Float): Dp = Dp(value = px / density)
fun Context.dpToPx(dp: Float): Float = dp * resources.displayMetrics.density
fun Context.pxToDp(px: Float): Float = (px / resources.displayMetrics.density).roundToInt().toFloat()
@Composable
fun Painter.dpSize(): DpSize = DpSize(
intrinsicSize.width.pxToDp().dp,
intrinsicSize.height.pxToDp().dp,
)
@Composable
private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this)

View file

@ -0,0 +1,20 @@
package com.tangem.core.ui.utils
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.drawable.toBitmap
/**
[REDACTED_AUTHOR]
*/
@Composable
fun asImageBitmap(@DrawableRes drawableId: Int): ImageBitmap {
val drawable = requireNotNull(AppCompatResources.getDrawable(LocalContext.current, drawableId)) {
"drawable is null"
}
return drawable.toBitmap().asImageBitmap()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24h-24z"/>
<path
android:pathData="M12,18L16,14L14.6,12.6L13,14.2V10H11V14.2L9.4,12.6L8,14L12,18ZM5,8V19H19V8H5ZM5,21C4.45,21 3.979,20.804 3.588,20.413C3.196,20.021 3,19.55 3,19V6.525C3,6.292 3.037,6.067 3.112,5.85C3.188,5.633 3.3,5.433 3.45,5.25L4.7,3.725C4.883,3.492 5.113,3.313 5.387,3.188C5.662,3.063 5.95,3 6.25,3H17.75C18.05,3 18.337,3.063 18.612,3.188C18.888,3.313 19.117,3.492 19.3,3.725L20.55,5.25C20.7,5.433 20.813,5.633 20.888,5.85C20.962,6.067 21,6.292 21,6.525V19C21,19.55 20.804,20.021 20.413,20.413C20.021,20.804 19.55,21 19,21H5ZM5.4,6H18.6L17.75,5H6.25L5.4,6Z"
android:fillColor="#000000"/>
</group>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M21.099,15C21.099,13.56 21.098,12.571 20.998,11.828C20.901,11.111 20.729,10.759 20.485,10.514C20.241,10.27 19.889,10.098 19.172,10.002C18.429,9.902 17.439,9.9 16,9.9H15C13.56,9.9 12.571,9.902 11.828,10.002C11.111,10.098 10.759,10.27 10.514,10.514C10.27,10.759 10.098,11.111 10.002,11.828C9.902,12.571 9.9,13.56 9.9,15V16C9.9,17.44 9.902,18.429 10.002,19.172C10.098,19.889 10.27,20.241 10.514,20.485C10.759,20.729 11.111,20.902 11.828,20.998C12.571,21.098 13.56,21.1 15,21.1H16C17.439,21.1 18.429,21.098 19.172,20.998C19.889,20.902 20.241,20.729 20.485,20.485C20.729,20.241 20.901,19.889 20.998,19.172C21.098,18.429 21.099,17.44 21.099,16V15ZM22.9,16C22.9,17.389 22.902,18.521 22.782,19.412C22.659,20.329 22.392,21.123 21.757,21.758C21.123,22.392 20.328,22.659 19.412,22.782C18.521,22.902 17.389,22.9 16,22.9H15C13.611,22.9 12.479,22.902 11.588,22.782C10.671,22.659 9.876,22.392 9.242,21.758C8.608,21.123 8.341,20.329 8.217,19.412C8.098,18.521 8.099,17.389 8.099,16V15C8.099,13.611 8.098,12.479 8.217,11.588C8.341,10.671 8.608,9.876 9.242,9.242C9.876,8.608 10.671,8.341 11.588,8.218C12.479,8.098 13.611,8.099 15,8.099H16C17.389,8.099 18.521,8.098 19.412,8.218C20.328,8.341 21.123,8.608 21.757,9.242C22.392,9.876 22.659,10.671 22.782,11.588C22.902,12.479 22.9,13.611 22.9,15V16Z"
android:fillColor="#919191"/>
<path
android:pathData="M2.099,9.999V8.999L2.099,8.945C2.099,7.581 2.099,6.466 2.217,5.587C2.34,4.67 2.607,3.876 3.241,3.241C3.876,2.607 4.67,2.34 5.587,2.217C6.466,2.099 7.581,2.099 8.945,2.099L8.999,2.099H9.999L10.054,2.099C11.417,2.099 12.532,2.099 13.411,2.217C14.328,2.34 15.123,2.607 15.757,3.241C15.978,3.463 16.16,3.709 16.307,3.983C16.542,4.42 16.378,4.966 15.941,5.201C15.503,5.437 14.957,5.273 14.722,4.835C14.656,4.713 14.579,4.608 14.484,4.514C14.24,4.27 13.888,4.098 13.171,4.001C12.428,3.901 11.439,3.9 9.999,3.9H8.999C7.559,3.9 6.57,3.901 5.827,4.001C5.11,4.098 4.758,4.27 4.514,4.514C4.269,4.758 4.097,5.11 4.001,5.827C3.901,6.57 3.899,7.559 3.899,8.999V9.999C3.899,11.439 3.901,12.428 4.001,13.171C4.097,13.888 4.269,14.24 4.514,14.484C4.692,14.663 4.923,14.797 5.292,14.895C5.772,15.022 6.059,15.515 5.932,15.995C5.812,16.446 5.371,16.726 4.921,16.654L4.831,16.635L4.606,16.569C4.09,16.404 3.633,16.149 3.241,15.757C2.607,15.123 2.34,14.328 2.217,13.411C2.099,12.532 2.099,11.417 2.099,10.054L2.099,9.999Z"
android:fillColor="#919191"/>
</vector>

View file

@ -0,0 +1,38 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="36dp"
android:height="37dp"
android:viewportWidth="36"
android:viewportHeight="37">
<path
android:pathData="M0,18.658C0,8.717 8.059,0.658 18,0.658C27.941,0.658 36,8.717 36,18.658C36,28.599 27.941,36.658 18,36.658C8.059,36.658 0,28.599 0,18.658Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="7.08"
android:startY="6.638"
android:endX="38.587"
android:endY="33.361"
android:type="linear">
<item android:offset="0" android:color="#FF8BA0FF"/>
<item android:offset="0.524" android:color="#FF65B1FF"/>
<item android:offset="1" android:color="#FF91D0FF"/>
</gradient>
</aapt:attr>
</path>
<group>
<clip-path
android:pathData="M9.314,8.766h17.372v19.784h-17.372z"/>
<path
android:pathData="M17.55,9.187L12.219,17.958C12.177,18.027 12.08,18.035 12.028,17.973C11.559,17.416 9.811,15.045 11.974,12.885C13.948,10.913 16.463,9.507 17.395,9.021C17.501,8.966 17.612,9.085 17.55,9.187Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M17.255,28.285C17.362,28.36 17.493,28.233 17.422,28.124C16.231,26.313 12.273,20.287 11.726,19.383C11.187,18.491 10.126,17.008 10.038,15.739C10.029,15.613 9.854,15.587 9.809,15.706C9.738,15.898 9.663,16.127 9.592,16.389C8.702,19.695 9.995,23.203 12.802,25.168L17.255,28.285Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M17.988,28.129L23.319,19.358C23.36,19.289 23.458,19.281 23.509,19.343C23.979,19.9 25.727,22.271 23.564,24.431C21.589,26.403 19.075,27.809 18.143,28.295C18.037,28.35 17.926,28.231 17.988,28.129Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M18.288,9.03C18.182,8.955 18.051,9.082 18.122,9.191C19.313,11.002 23.271,17.028 23.818,17.932C24.357,18.824 25.418,20.307 25.506,21.576C25.515,21.702 25.69,21.728 25.734,21.609C25.805,21.417 25.881,21.188 25.951,20.926C26.841,17.62 25.549,14.112 22.742,12.147L18.288,9.03Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View file

@ -0,0 +1,9 @@
<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="M20,10.912C20,17.156 16.358,19.16 14.439,17.376C12.86,15.921 12.39,12.847 10.014,12.531C6.999,12.133 6.738,16.346 4.754,16.346C2.444,16.346 2,12.819 2,11.008C2,9.155 2.496,6.63 4.467,6.63C6.764,6.63 6.895,10.24 9.767,10.047C12.625,9.842 12.677,6.081 14.531,4.476C16.149,3.09 20,4.585 20,10.912Z"
android:fillColor="#000000"/>
</vector>

Some files were not shown because too many files have changed in this diff Show more