Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-22 10:09:11 +02:00
commit e4754fed3b
483 changed files with 9315 additions and 4084 deletions

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

@ -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

@ -12,6 +12,7 @@ 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.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
@ -23,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
@ -102,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

@ -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) {
@ -58,7 +59,7 @@ fun PinTextField(
}
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
keyboardType = KeyboardType.NumberPassword,
imeAction = ImeAction.Done,
),
singleLine = true,
@ -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

@ -27,7 +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.SwapTokenScreenTestTags
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)
@ -67,20 +67,23 @@ fun InputRowDefault(
Column(
modifier = Modifier
.weight(1f)
.testTag(SwapTokenScreenTestTags.NETWORK_FEE_BLOCK),
.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

@ -76,7 +76,7 @@ fun InputRowRecipient(
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,

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

@ -51,7 +51,6 @@ private const val DISABLED_ICON_ALPHA = 0.4f
fun ProviderChooseCrypto(providerChooseUM: ProviderChooseUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
ConstraintLayout(
modifier = modifier
.clip(RoundedCornerShape(14.dp))
.selectedBorder(isSelected = providerChooseUM.isSelected)
.clickable(
enabled = !providerChooseUM.hasError(),

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
@ -23,7 +22,7 @@ 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
@ -31,7 +30,7 @@ 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),
@ -80,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),
@ -151,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

@ -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

@ -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,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

@ -6,7 +6,6 @@ object SwapTokenScreenTestTags {
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 NETWORK_FEE_BLOCK = "SWAP_TOKEN_SCREEN_NETWORK_FEE_BLOCK"
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"

View file

@ -2,7 +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

@ -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

@ -0,0 +1,19 @@
<vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:pathData="M6.839,11.095C7.657,11.095 8.32,11.759 8.32,12.576V20.148C8.32,20.966 7.657,21.629 6.839,21.629H2.864C2.046,21.629 1.383,20.966 1.383,20.148V12.576C1.383,11.759 2.046,11.095 2.864,11.095H6.839ZM14.064,2.37C14.831,2.37 15.401,2.37 15.894,2.502C17.227,2.858 18.268,3.897 18.625,5.226C18.709,5.54 18.74,5.885 18.751,6.291C18.895,6.304 19.034,6.319 19.167,6.337C20.062,6.457 20.853,6.719 21.486,7.351C22.119,7.983 22.382,8.772 22.503,9.665C22.618,10.516 22.617,11.591 22.617,12.895V14.957C22.617,16.261 22.618,17.336 22.503,18.187C22.382,19.08 22.12,19.869 21.486,20.501C20.853,21.133 20.062,21.395 19.167,21.515C18.314,21.629 17.236,21.629 15.929,21.629H9.999C9.987,21.629 9.975,21.628 9.963,21.628C10.176,21.18 10.297,20.678 10.297,20.149V12.577C10.297,10.668 8.749,9.12 6.84,9.12H2.865C2.335,9.12 1.832,9.24 1.383,9.453V5.259C1.383,3.664 2.679,2.37 4.278,2.37H14.064ZM18.406,12.436C17.468,12.436 16.707,13.196 16.707,14.134C16.707,15.073 17.468,15.833 18.406,15.833C19.345,15.833 20.105,15.073 20.105,14.134C20.105,13.196 19.344,12.436 18.406,12.436ZM4.159,12.573C3.723,12.573 3.369,12.926 3.369,13.363C3.369,13.799 3.723,14.153 4.159,14.153H5.542C5.978,14.153 6.332,13.799 6.332,13.363C6.332,12.926 5.978,12.573 5.542,12.573H4.159ZM4.278,4.296C3.745,4.296 3.313,4.727 3.313,5.259C3.313,5.791 3.745,6.222 4.278,6.222H15.929C16.237,6.222 16.533,6.222 16.816,6.224C16.808,5.975 16.791,5.839 16.76,5.724C16.581,5.059 16.061,4.54 15.394,4.362C15.18,4.304 14.889,4.296 13.93,4.296H4.278Z">
<aapt:attr name="android:fillColor">
<gradient android:centerX="7.343" android:centerY="0.784" android:gradientRadius="33.786" android:type="radial">
<item android:color="#FF96999B" android:offset="0"/>
<item android:color="#FF4D4E53" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="33dp"
android:viewportWidth="32"
android:viewportHeight="33">
<path
android:pathData="M16,4.062C17.587,4.062 19.084,4.339 20.489,4.893C21.894,5.447 23.151,6.22 24.259,7.212C25.366,8.204 26.272,9.361 26.974,10.684C27.677,11.998 28.119,13.425 28.301,14.962H30.26C30.5,14.962 30.682,15.012 30.806,15.111C30.93,15.21 30.988,15.342 30.98,15.508C30.971,15.665 30.901,15.843 30.769,16.041L28.066,19.885C27.892,20.133 27.694,20.257 27.47,20.257C27.255,20.249 27.065,20.125 26.9,19.885L24.197,16.029C24.064,15.839 23.994,15.665 23.986,15.508C23.986,15.342 24.048,15.21 24.172,15.111C24.296,15.012 24.474,14.962 24.705,14.962H26.677C26.495,13.648 26.094,12.428 25.474,11.304C24.862,10.172 24.077,9.184 23.118,8.34C22.159,7.497 21.072,6.84 19.857,6.369C18.65,5.898 17.364,5.662 16,5.662C14.405,5.662 12.904,5.988 11.499,6.642C10.101,7.286 8.903,8.171 7.903,9.295C7.712,9.502 7.518,9.614 7.32,9.63C7.121,9.638 6.944,9.585 6.786,9.469C6.613,9.345 6.505,9.167 6.464,8.936C6.423,8.696 6.497,8.469 6.687,8.254C7.836,6.956 9.217,5.935 10.829,5.191C12.441,4.438 14.165,4.062 16,4.062ZM16,28.938C14.413,28.938 12.916,28.656 11.511,28.094C10.106,27.54 8.849,26.767 7.741,25.775C6.634,24.792 5.724,23.642 5.013,22.328C4.311,21.005 3.868,19.575 3.686,18.038H1.727C1.487,18.038 1.31,17.988 1.194,17.889C1.07,17.789 1.012,17.657 1.02,17.492C1.02,17.327 1.086,17.149 1.219,16.959L3.922,13.102C4.096,12.862 4.29,12.743 4.505,12.743C4.728,12.743 4.926,12.862 5.1,13.102L7.803,16.971C7.936,17.161 8.002,17.339 8.002,17.504C8.002,17.661 7.94,17.789 7.816,17.889C7.7,17.988 7.522,18.038 7.283,18.038H5.323C5.497,19.344 5.894,20.563 6.514,21.696C7.134,22.82 7.919,23.808 8.87,24.659C9.829,25.503 10.912,26.16 12.119,26.631C13.334,27.102 14.628,27.338 16,27.338C17.587,27.338 19.079,27.011 20.476,26.358C21.882,25.713 23.089,24.825 24.097,23.692C24.288,23.494 24.478,23.39 24.668,23.382C24.866,23.366 25.044,23.411 25.201,23.518C25.375,23.651 25.482,23.837 25.524,24.076C25.565,24.308 25.49,24.531 25.3,24.746C24.16,26.036 22.779,27.057 21.159,27.809C19.546,28.561 17.827,28.938 16,28.938Z"
android:fillColor="#0099FF"/>
<path
android:pathData="M16.153,10.844C17.725,10.924 18.975,12.223 18.975,13.815V15.295C19.876,15.317 20.6,16.055 20.6,16.961V20.492C20.6,21.413 19.853,22.159 18.933,22.159H13.069C12.148,22.159 11.402,21.413 11.402,20.492V16.961C11.402,16.055 12.125,15.318 13.026,15.295V13.815C13.026,12.172 14.357,10.84 16,10.84L16.153,10.844ZM16,12.23C15.125,12.23 14.415,12.939 14.415,13.815V15.294H17.586V13.815C17.586,12.939 16.876,12.23 16,12.23Z"
android:fillColor="#0099FF"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="56dp"
android:height="56dp"
android:viewportWidth="56"
android:viewportHeight="56">
<path
android:pathData="M49.875,26.906V20.475C49.875,17.902 49.875,16.616 49.377,15.633C48.939,14.769 48.24,14.066 47.381,13.626C46.403,13.125 45.124,13.125 42.566,13.125H13.434C10.876,13.125 9.597,13.125 8.619,13.626C7.76,14.066 7.061,14.769 6.623,15.633C6.125,16.616 6.125,17.902 6.125,20.475V28.744C6.125,31.316 6.125,32.603 6.623,33.585C7.061,34.45 7.76,35.153 8.619,35.593C9.597,36.094 10.876,36.094 13.434,36.094H25.716M28,24.609H28.011M38.546,24.609H38.557M17.454,24.609H17.466M28.571,24.609C28.571,24.927 28.315,25.184 28,25.184C27.685,25.184 27.429,24.927 27.429,24.609C27.429,24.292 27.685,24.035 28,24.035C28.315,24.035 28.571,24.292 28.571,24.609ZM39.117,24.609C39.117,24.927 38.861,25.184 38.546,25.184C38.23,25.184 37.975,24.927 37.975,24.609C37.975,24.292 38.23,24.035 38.546,24.035C38.861,24.035 39.117,24.292 39.117,24.609ZM18.025,24.609C18.025,24.927 17.77,25.184 17.454,25.184C17.139,25.184 16.883,24.927 16.883,24.609C16.883,24.292 17.139,24.035 17.454,24.035C17.77,24.035 18.025,24.292 18.025,24.609Z"
android:strokeLineJoin="round"
android:strokeWidth="3"
android:fillColor="#00000000"
android:strokeColor="#008EFF"
android:strokeLineCap="round"/>
<path
android:pathData="M45.242,38.664V34.645C45.242,32.425 43.452,30.625 41.244,30.625C39.037,30.625 37.247,32.425 37.247,34.645V38.664M36.905,47.852H45.584C46.864,47.852 47.503,47.852 47.992,47.601C48.422,47.381 48.771,47.03 48.99,46.597C49.239,46.106 49.239,45.463 49.239,44.177V42.339C49.239,41.053 49.239,40.41 48.99,39.918C48.771,39.486 48.422,39.135 47.992,38.914C47.503,38.664 46.864,38.664 45.584,38.664H36.905C35.625,38.664 34.986,38.664 34.497,38.914C34.067,39.135 33.718,39.486 33.499,39.918C33.25,40.41 33.25,41.053 33.25,42.339V44.177C33.25,45.463 33.25,46.106 33.499,46.597C33.718,47.03 34.067,47.381 34.497,47.601C34.986,47.852 35.625,47.852 36.905,47.852Z"
android:strokeLineJoin="round"
android:strokeWidth="3"
android:fillColor="#00000000"
android:strokeColor="#008EFF"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#0099FF"
android:pathData="M16.539,8.682C16.769,8.338 16.677,7.872 16.332,7.642C15.988,7.412 15.522,7.505 15.292,7.849L10.88,14.452L8.677,11.614C8.423,11.287 7.952,11.228 7.625,11.482C7.297,11.736 7.238,12.207 7.492,12.534L10.333,16.194C10.481,16.384 10.712,16.492 10.953,16.484C11.193,16.475 11.416,16.351 11.55,16.151L16.539,8.682Z" />
<path
android:fillColor="#0099FF"
android:fillType="evenOdd"
android:pathData="M12,1.746C6.337,1.746 1.746,6.337 1.746,12C1.746,17.663 6.337,22.254 12,22.254C17.663,22.254 22.254,17.663 22.254,12C22.254,6.337 17.663,1.746 12,1.746ZM3.246,12C3.246,7.165 7.165,3.246 12,3.246C16.835,3.246 20.754,7.165 20.754,12C20.754,16.835 16.835,20.754 12,20.754C7.165,20.754 3.246,16.835 3.246,12Z" />
</vector>