Updated on 2026-08-14
This commit is contained in:
commit
cc3401a412
943 changed files with 15667 additions and 6451 deletions
|
|
@ -220,6 +220,8 @@ fun SecondaryButtonIconEnd(
|
|||
modifier: Modifier = Modifier,
|
||||
showProgress: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
size: TangemButtonSize = TangemButtonSize.Default,
|
||||
shape: Shape = size.toShape(),
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
|
|
@ -230,6 +232,8 @@ fun SecondaryButtonIconEnd(
|
|||
enabled = enabled,
|
||||
showProgress = showProgress,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
size = size,
|
||||
shape = shape,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -244,6 +248,8 @@ fun SecondaryButtonIconStart(
|
|||
modifier: Modifier = Modifier,
|
||||
showProgress: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
size: TangemButtonSize = TangemButtonSize.Default,
|
||||
shape: Shape = size.toShape(),
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
|
|
@ -254,6 +260,8 @@ fun SecondaryButtonIconStart(
|
|||
enabled = enabled,
|
||||
showProgress = showProgress,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
size = size,
|
||||
shape = shape,
|
||||
)
|
||||
}
|
||||
// endregion SecondaryButton
|
||||
|
|
|
|||
|
|
@ -37,44 +37,46 @@ fun DialogFullScreen(
|
|||
decorFitsSystemWindows = false,
|
||||
),
|
||||
content = {
|
||||
val activityWindow = getActivityWindow()
|
||||
val dialogWindow = getDialogWindow()
|
||||
val parentView = LocalView.current.parent as View
|
||||
SideEffect {
|
||||
if (activityWindow != null && dialogWindow != null) {
|
||||
val attributes = WindowManager.LayoutParams().apply {
|
||||
copyFrom(activityWindow.attributes)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
} else {
|
||||
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
|
||||
}
|
||||
type = dialogWindow.attributes.type
|
||||
}
|
||||
|
||||
dialogWindow.attributes = attributes
|
||||
parentView.layoutParams =
|
||||
FrameLayout.LayoutParams(
|
||||
activityWindow.decorView.width,
|
||||
activityWindow.decorView.height,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
val systemUiController = rememberSystemUiController(getActivityWindow())
|
||||
val dialogSystemUiController = rememberSystemUiController(getDialogWindow())
|
||||
|
||||
ProvideSystemBarsIconsController {
|
||||
val activityWindow = getActivityWindow()
|
||||
val dialogWindow = getDialogWindow()
|
||||
val parentView = LocalView.current.parent as View
|
||||
SideEffect {
|
||||
systemUiController.setSystemBarsColor(color = Color.Transparent)
|
||||
dialogSystemUiController.setSystemBarsColor(color = Color.Transparent)
|
||||
if (activityWindow != null && dialogWindow != null) {
|
||||
val attributes = WindowManager.LayoutParams().apply {
|
||||
copyFrom(activityWindow.attributes)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
} else {
|
||||
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
|
||||
}
|
||||
type = dialogWindow.attributes.type
|
||||
}
|
||||
|
||||
dialogWindow.attributes = attributes
|
||||
parentView.layoutParams =
|
||||
FrameLayout.LayoutParams(
|
||||
activityWindow.decorView.width,
|
||||
activityWindow.decorView.height,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not())
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
val systemUiController = rememberSystemUiController(getActivityWindow())
|
||||
val dialogSystemUiController = rememberSystemUiController(getDialogWindow())
|
||||
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) {
|
||||
content()
|
||||
SideEffect {
|
||||
systemUiController.setSystemBarsColor(color = Color.Transparent)
|
||||
dialogSystemUiController.setSystemBarsColor(color = Color.Transparent)
|
||||
}
|
||||
}
|
||||
|
||||
SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not())
|
||||
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -308,13 +308,14 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun DialogButtons(
|
||||
confirmButton: DialogButtonUM,
|
||||
dismissButton: DialogButtonUM?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = TangemTheme.dimens.spacing4,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ data class MessageBottomSheetUMV2(
|
|||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class IconImage(@DrawableRes internal var res: Int) : Element
|
||||
|
||||
@Immutable
|
||||
data class Chip(
|
||||
internal var text: TextReference,
|
||||
|
|
@ -54,6 +57,7 @@ data class MessageBottomSheetUMV2(
|
|||
@Immutable
|
||||
data class InfoBlock(
|
||||
internal var icon: Icon? = null,
|
||||
internal var iconImage: IconImage? = null,
|
||||
internal var chip: Chip? = null,
|
||||
var title: TextReference? = null,
|
||||
var body: TextReference? = null,
|
||||
|
|
@ -106,6 +110,10 @@ fun MessageBottomSheetUMV2.InfoBlock.icon(@DrawableRes res: Int, init: MessageBo
|
|||
icon = MessageBottomSheetUMV2.Icon(res).apply(init)
|
||||
}
|
||||
|
||||
fun MessageBottomSheetUMV2.InfoBlock.iconImage(@DrawableRes res: Int) = apply {
|
||||
iconImage = MessageBottomSheetUMV2.IconImage(res)
|
||||
}
|
||||
|
||||
fun MessageBottomSheetUMV2.InfoBlock.chip(text: TextReference, init: MessageBottomSheetUMV2.Chip.() -> Unit = {}) =
|
||||
apply {
|
||||
chip = MessageBottomSheetUMV2.Chip(text).apply(init)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.core.ui.components.bottomsheets.message
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -25,6 +25,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi
|
|||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.core.ui.components.icons.HighlightedIcon
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -91,9 +92,7 @@ fun MessageBottomSheetV2Content(state: MessageBottomSheetUMV2, modifier: Modifie
|
|||
@Composable
|
||||
private fun ContentContainer(state: MessageBottomSheetUMV2.InfoBlock, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
state.icon?.let {
|
||||
BottomSheetIcon(it)
|
||||
}
|
||||
BottomSheetIconContainer(state.icon, state.iconImage)
|
||||
state.title?.let { title ->
|
||||
Text(
|
||||
modifier = Modifier
|
||||
|
|
@ -125,6 +124,26 @@ private fun ContentContainer(state: MessageBottomSheetUMV2.InfoBlock, modifier:
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("CanBeNonNullable")
|
||||
@Composable
|
||||
private fun BottomSheetIconContainer(
|
||||
icon: MessageBottomSheetUMV2.Icon?,
|
||||
iconImage: MessageBottomSheetUMV2.IconImage?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (icon != null) {
|
||||
BottomSheetIcon(icon, modifier)
|
||||
} else if (iconImage != null) {
|
||||
Image(
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size56)
|
||||
.clip(CircleShape),
|
||||
painter = painterResource(id = iconImage.res),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetIcon(icon: MessageBottomSheetUMV2.Icon, modifier: Modifier = Modifier) {
|
||||
val tint = when (icon.type) {
|
||||
|
|
@ -144,20 +163,11 @@ private fun BottomSheetIcon(icon: MessageBottomSheetUMV2.Icon, modifier: Modifie
|
|||
MessageBottomSheetUMV2.Icon.BackgroundType.Warning -> TangemTheme.colors.icon.warning
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size56)
|
||||
.clip(CircleShape)
|
||||
.background(backgroundColor.copy(alpha = 0.1F)),
|
||||
contentAlignment = Alignment.Center,
|
||||
content = {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size32),
|
||||
painter = painterResource(icon.res),
|
||||
contentDescription = null,
|
||||
tint = tint,
|
||||
)
|
||||
},
|
||||
HighlightedIcon(
|
||||
modifier = modifier,
|
||||
icon = icon.res,
|
||||
iconTint = tint,
|
||||
backgroundColor = backgroundColor,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -248,4 +258,33 @@ private fun Preview() {
|
|||
onDismissRequest = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview2() {
|
||||
TangemThemePreview {
|
||||
MessageBottomSheetV2(
|
||||
messageBottomSheetUM {
|
||||
infoBlock {
|
||||
iconImage = MessageBottomSheetUMV2.IconImage(R.drawable.img_visa_notification)
|
||||
title = TextReference.Str("Title Title Title")
|
||||
body = TextReference.Str("Body")
|
||||
chip(text = TextReference.Str("Some chip information"))
|
||||
}
|
||||
primaryButton {
|
||||
text = TextReference.Str("Test")
|
||||
icon = R.drawable.ic_tangem_24
|
||||
}
|
||||
secondaryButton {
|
||||
icon = R.drawable.ic_tangem_24
|
||||
text = TextReference.Str("asdasd")
|
||||
onClick {
|
||||
closeBs()
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissRequest = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.ui.components.bottomsheets.state
|
||||
|
||||
enum class BottomSheetState {
|
||||
EXPANDED,
|
||||
COLLAPSED,
|
||||
}
|
||||
|
|
@ -39,8 +39,8 @@ fun PinTextField(
|
|||
pinTextColor: PinTextColor,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val textFieldValue = remember(value) {
|
||||
TextFieldValue(value, selection = TextRange(value.length))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.core.ui.components.icons
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
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.res.painterResource
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun HighlightedIcon(
|
||||
@DrawableRes icon: Int,
|
||||
iconTint: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = iconTint,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size56)
|
||||
.clip(CircleShape)
|
||||
.background(backgroundColor.copy(alpha = 0.1F)),
|
||||
contentAlignment = Alignment.Center,
|
||||
content = {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size32),
|
||||
painter = painterResource(icon),
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
|
|
@ -18,10 +19,16 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelSize
|
||||
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
|
||||
|
|
@ -37,6 +44,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
*
|
||||
* @see <a href="https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=4480-1459&t=2QTpi1G7FeTexTFS-4">Figma</a>
|
||||
*/
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun Label(state: LabelUM, modifier: Modifier = Modifier) {
|
||||
val backgroundColor by animateColorAsState(
|
||||
|
|
@ -63,12 +71,28 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
|
|||
},
|
||||
)
|
||||
|
||||
AnimatedContent(targetState = state.text) { text ->
|
||||
val horizontalArrangementSize = remember {
|
||||
when (state.size) {
|
||||
LabelSize.REGULAR -> 4.dp
|
||||
LabelSize.BIG -> 8.dp
|
||||
}
|
||||
}
|
||||
|
||||
val paddings = remember {
|
||||
when (state.size) {
|
||||
LabelSize.REGULAR -> PaddingValues(horizontal = 8.dp, vertical = 4.dp)
|
||||
LabelSize.BIG -> PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
modifier = modifier,
|
||||
targetState = state.text,
|
||||
) { text ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(horizontalArrangementSize),
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCorners8)
|
||||
.background(color = backgroundColor)
|
||||
.then(
|
||||
|
|
@ -82,8 +106,34 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
|
|||
Modifier
|
||||
},
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
.padding(paddings),
|
||||
) {
|
||||
state.leadingContent.let { leadingContentUM ->
|
||||
when (leadingContentUM) {
|
||||
is LabelLeadingContentUM.Token -> {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier.size(16.dp),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(leadingContentUM.iconUrl)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(enable = false)
|
||||
.build(),
|
||||
loading = { CircleShimmer() },
|
||||
error = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.tertiary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
LabelLeadingContentUM.None -> Unit
|
||||
}
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.weight(1.0f, fill = false),
|
||||
text = text.resolveReference(),
|
||||
|
|
@ -109,6 +159,8 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
|
|
@ -118,47 +170,93 @@ private fun LabelPreview() {
|
|||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Regular Label"),
|
||||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Accent Label"),
|
||||
style = LabelStyle.ACCENT,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Warning Label"),
|
||||
style = LabelStyle.WARNING,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str(
|
||||
"Regular long long long long long long long long long long long long Label",
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Regular Label"),
|
||||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
style = LabelStyle.REGULAR,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Accent Label"),
|
||||
style = LabelStyle.ACCENT,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Warning Label"),
|
||||
style = LabelStyle.WARNING,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
leadingContent = LabelLeadingContentUM.Token(
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/euro-coin.png",
|
||||
),
|
||||
text = TextReference.Str("Regular Label"),
|
||||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Accent Label"),
|
||||
style = LabelStyle.ACCENT,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Warning Label"),
|
||||
style = LabelStyle.WARNING,
|
||||
),
|
||||
)
|
||||
}
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str(
|
||||
"Regular long long long long long long long long long long long long Label",
|
||||
),
|
||||
style = LabelStyle.REGULAR,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Accent Label"),
|
||||
style = LabelStyle.ACCENT,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Warning Label"),
|
||||
style = LabelStyle.WARNING,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
}
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Regular Label"),
|
||||
style = LabelStyle.REGULAR,
|
||||
size = LabelSize.BIG,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Accent Label"),
|
||||
style = LabelStyle.ACCENT,
|
||||
size = LabelSize.BIG,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Warning Label"),
|
||||
style = LabelStyle.WARNING,
|
||||
size = LabelSize.BIG,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,29 @@
|
|||
package com.tangem.core.ui.components.label.entity
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
data class LabelUM(
|
||||
val text: TextReference,
|
||||
val style: LabelStyle,
|
||||
val style: LabelStyle = LabelStyle.REGULAR,
|
||||
val size: LabelSize = LabelSize.REGULAR,
|
||||
val leadingContent: LabelLeadingContentUM = LabelLeadingContentUM.None,
|
||||
@DrawableRes val icon: Int? = null,
|
||||
val onIconClick: (() -> Unit)? = null,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
sealed class LabelLeadingContentUM {
|
||||
data object None : LabelLeadingContentUM()
|
||||
data class Token(val iconUrl: String) : LabelLeadingContentUM()
|
||||
}
|
||||
|
||||
enum class LabelStyle {
|
||||
REGULAR, ACCENT, WARNING,
|
||||
}
|
||||
|
||||
enum class LabelSize {
|
||||
REGULAR, BIG,
|
||||
}
|
||||
|
|
@ -296,6 +296,7 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
size = TangemButtonSize.WideAction,
|
||||
enabled = isEnabled,
|
||||
showProgress = config.shouldShowProgress,
|
||||
)
|
||||
} else {
|
||||
PrimaryButton(
|
||||
|
|
@ -304,6 +305,7 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
size = TangemButtonSize.WideAction,
|
||||
enabled = isEnabled,
|
||||
showProgress = config.shouldShowProgress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ data class NotificationConfig(
|
|||
val additionalText: TextReference? = null,
|
||||
@DrawableRes val iconResId: Int? = null,
|
||||
val onClick: () -> Unit,
|
||||
val shouldShowProgress: Boolean = false,
|
||||
) : ButtonsState()
|
||||
|
||||
data class SecondaryButtonConfig(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
package com.tangem.core.ui.components.pager
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.*
|
||||
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.graphics.Shape
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
/**
|
||||
* Horizontal pager indicator
|
||||
*
|
||||
* @param pagerState state of pager
|
||||
* @param indicatorCount counter of visible indicator items
|
||||
*/
|
||||
@Composable
|
||||
fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier, indicatorCount: Int = 5) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val indicatorColor = TangemTheme.colors.control.key
|
||||
val overlayColor = TangemTheme.colors.overlay.secondary
|
||||
val indicatorSize = 8.dp
|
||||
val spacing = 4.dp
|
||||
|
||||
val totalWidth: Dp = indicatorSize * indicatorCount + spacing * (indicatorCount - 1)
|
||||
val widthInPx = LocalDensity.current.run { indicatorSize.toPx() }
|
||||
|
||||
val currentItem by remember {
|
||||
derivedStateOf {
|
||||
pagerState.currentPage
|
||||
}
|
||||
}
|
||||
|
||||
val itemCount = pagerState.pageCount
|
||||
|
||||
LaunchedEffect(key1 = currentItem) {
|
||||
val viewportSize = listState.layoutInfo.viewportSize
|
||||
listState.animateScrollToItem(
|
||||
currentItem,
|
||||
(widthInPx / 2 - viewportSize.width / 2).toInt(),
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(32.dp)
|
||||
.background(
|
||||
color = overlayColor,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.width(totalWidth),
|
||||
state = listState,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
userScrollEnabled = false,
|
||||
) {
|
||||
indicatorItems(
|
||||
itemCount = itemCount,
|
||||
currentItem = currentItem,
|
||||
indicatorShape = CircleShape,
|
||||
activeColor = indicatorColor,
|
||||
inActiveColor = indicatorColor.copy(alpha = 0.5f),
|
||||
indicatorSize = indicatorSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun LazyListScope.indicatorItems(
|
||||
itemCount: Int,
|
||||
currentItem: Int,
|
||||
indicatorShape: Shape,
|
||||
activeColor: Color,
|
||||
inActiveColor: Color,
|
||||
indicatorSize: Dp,
|
||||
) {
|
||||
items(itemCount) { index ->
|
||||
|
||||
val isSelected = index == currentItem
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(indicatorShape)
|
||||
.size(indicatorSize)
|
||||
.background(
|
||||
if (isSelected) activeColor else inActiveColor,
|
||||
indicatorShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicatorPreviewFirstPage() {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = 0,
|
||||
pageCount = { 10 },
|
||||
)
|
||||
PagerIndicator(pagerState = pagerState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -246,7 +246,8 @@ data class EventMessageAction(
|
|||
*
|
||||
* @param onClick The action to perform when the button is clicked. By default, it dismisses the message.
|
||||
* */
|
||||
fun cancelAction(onClick: () -> Unit = onDismissRequest) = EventMessageAction(
|
||||
fun cancelAction(isWarning: Boolean = false, onClick: () -> Unit = onDismissRequest) = EventMessageAction(
|
||||
isWarning = isWarning,
|
||||
title = resourceReference(id = R.string.common_cancel),
|
||||
onClick = onClick,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,30 @@ object Dialogs {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hot wallet creation not supported dialog
|
||||
*
|
||||
* @param leastSupportedVersion least supported OS version name ex. "Android 10"
|
||||
* @param onDismiss lambda be invoked when dialog is dismissed
|
||||
*/
|
||||
fun hotWalletCreationNotSupportedDialog(leastSupportedVersion: String, onDismiss: () -> Unit = {}): DialogMessage {
|
||||
return DialogMessage(
|
||||
title = resourceReference(
|
||||
id = R.string.mobile_wallet_requires_min_os_warning_title,
|
||||
formatArgs = wrappedList(leastSupportedVersion),
|
||||
),
|
||||
message = resourceReference(
|
||||
id = R.string.mobile_wallet_requires_min_os_warning_body,
|
||||
formatArgs = wrappedList(leastSupportedVersion),
|
||||
),
|
||||
firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_got_it),
|
||||
onClick = {},
|
||||
),
|
||||
onDismissRequest = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal error dialog
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ package com.tangem.core.ui.test
|
|||
object DetailsScreenTestTags {
|
||||
const val SCREEN_CONTAINER = "DETAILS_SCREEN_CONTAINER"
|
||||
const val SCREEN_ITEM = "DETAILS_SCREEN_ITEM"
|
||||
const val VERSION_NAME = "DETAILS_SCREEN_VERSION_NAME"
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ object SendScreenTestTags {
|
|||
|
||||
const val AMOUNT_CONTAINER_TITLE = "SEND_SCREEN_AMOUNT_CONTAINER_TITLE"
|
||||
const val INPUT_TEXT_FIELD = "SEND_SCREEN_INPUT_TEXT_FIELD"
|
||||
const val AMOUNT_ERROR_TEXT = "SEND_SCREEN_AMOUNT_ERROR_TEXT"
|
||||
const val EQUIVALENT_INPUT_AMOUNT = "SEND_SCREEN_EQUIVALENT_INPUT_AMOUNT"
|
||||
const val EXCHANGE_ICON = "SEND_SCREEN_EXCHANGE_ICON"
|
||||
const val TOKEN_NAME = "SEND_SCREEN_TOKEN_NAME"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters.dateDDMMYYYY
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters.dateMMMdd
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters.dateTimeFormatter
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters.dateYYYY
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.joda.time.format.DateTimeFormat
|
||||
import org.joda.time.format.DateTimeFormatter
|
||||
import org.joda.time.format.DateTimeFormatterBuilder
|
||||
|
|
@ -80,6 +85,13 @@ object DateTimeFormatters {
|
|||
getBestFormatterBySkeleton("yyyy")
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: "June 31"
|
||||
*/
|
||||
val dateDMMM: DateTimeFormatter by lazy {
|
||||
getBestFormatterBySkeleton("d MMMM")
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: "31.06.2020 12:00", "06/31/2020 12:00", "06/31/2020 12:00 PM"
|
||||
*/
|
||||
|
|
@ -87,6 +99,23 @@ object DateTimeFormatters {
|
|||
getBestFormatterBySkeleton("dd.MM.yyyy HH:mm")
|
||||
}
|
||||
|
||||
/**
|
||||
* Local full date formatter (e.g., "dd MMMM, HH:mm")
|
||||
*/
|
||||
val localFullDate: DateTimeFormatter by lazy {
|
||||
DateTimeFormatterBuilder()
|
||||
.appendDayOfMonth(2)
|
||||
.appendLiteral(' ')
|
||||
.appendMonthOfYearText()
|
||||
.appendLiteral(", ")
|
||||
.appendHourOfDay(2)
|
||||
.appendLiteral(':')
|
||||
.appendMinuteOfHour(2)
|
||||
.toFormatter()
|
||||
.withLocale(Locale.getDefault())
|
||||
.withZone(DateTimeZone.getDefault())
|
||||
}
|
||||
|
||||
fun formatDate(date: DateTime, formatter: DateTimeFormatter = dateFormatter): String {
|
||||
return formatter.print(date)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,4 +37,51 @@ fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeForm
|
|||
*/
|
||||
fun Long.formatAsDateTime(formatter: DateTimeFormatter): String {
|
||||
return DateTimeFormatters.formatDate(date = DateTime(this, DateTimeZone.getDefault()), formatter = formatter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an ISO 8601 date string and compares it to the current UTC time.
|
||||
*
|
||||
|
||||
* @param now The current date to compare against.
|
||||
* @return A [FormattedDate] subclass.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun getFormattedDate(createdAt: String, now: DateTime): FormattedDate {
|
||||
val pastDateUtc = try {
|
||||
DateTime.parse(createdAt)
|
||||
} catch (_: Exception) {
|
||||
return FormattedDate.FullDate(createdAt)
|
||||
}
|
||||
|
||||
val pastDateLocal = pastDateUtc.withZone(DateTimeZone.getDefault())
|
||||
val isToday = pastDateLocal.isToday()
|
||||
|
||||
val diffInMillis = now.millis - pastDateUtc.millis
|
||||
val diffInMinutes = diffInMillis / (1000 * 60)
|
||||
val diffInHours = diffInMillis / (1000 * 60 * 60)
|
||||
|
||||
return when {
|
||||
diffInMinutes < 1 -> FormattedDate.MinutesAgo(1)
|
||||
diffInMinutes < 60 -> FormattedDate.MinutesAgo(diffInMinutes.toInt())
|
||||
diffInHours < 12 && isToday -> FormattedDate.HoursAgo(diffInHours.toInt())
|
||||
isToday -> {
|
||||
val timeString = DateTimeFormatters.timeFormatter.print(pastDateLocal)
|
||||
FormattedDate.Today(timeString)
|
||||
}
|
||||
else -> {
|
||||
val dateString = DateTimeFormatters.localFullDate.print(pastDateLocal)
|
||||
FormattedDate.FullDate(dateString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Representing different formatted date representations.
|
||||
*/
|
||||
sealed class FormattedDate {
|
||||
data class MinutesAgo(val minutes: Int) : FormattedDate()
|
||||
data class HoursAgo(val hours: Int) : FormattedDate()
|
||||
data class Today(val time: String) : FormattedDate()
|
||||
data class FullDate(val date: String) : FormattedDate()
|
||||
}
|
||||
13
core/ui/src/main/res/drawable/ic_analytics_up_mini_24.xml
Normal file
13
core/ui/src/main/res/drawable/ic_analytics_up_mini_24.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="40" android:viewportWidth="40" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M15.849,26.896C16.954,26.896 17.849,27.728 17.85,28.753V33.615C17.849,34.641 16.954,35.473 15.849,35.473C14.745,35.473 13.85,34.641 13.849,33.615V28.753C13.85,27.727 14.745,26.896 15.849,26.896Z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M32.454,20.958C33.558,20.958 34.454,21.819 34.454,22.879V33.552C34.454,34.612 33.558,35.473 32.454,35.473C31.35,35.473 30.454,34.612 30.454,33.552V22.879C30.454,21.818 31.349,20.958 32.454,20.958Z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M7.547,29.467C8.651,29.467 9.547,30.362 9.547,31.467V33.472C9.547,34.576 8.651,35.472 7.547,35.472C6.442,35.472 5.547,34.576 5.547,33.472V31.467C5.547,30.362 6.442,29.467 7.547,29.467Z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M24.151,23.976C25.256,23.976 26.151,24.755 26.151,25.716V33.732C26.151,34.693 25.256,35.472 24.151,35.472C23.047,35.472 22.151,34.693 22.151,33.732V25.716C22.152,24.755 23.047,23.976 24.151,23.976Z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M29.46,4.062C29.927,4.036 30.392,4.174 30.77,4.457C31.203,4.781 31.486,5.266 31.556,5.801L32.671,14.396C32.813,15.491 32.04,16.495 30.944,16.637C29.849,16.779 28.846,16.006 28.704,14.911L28.228,11.251C26.87,12.973 25.156,14.988 23.389,16.644C19.165,20.601 14.718,22.22 9.224,21.976C8.12,21.927 7.265,20.992 7.314,19.889C7.363,18.786 8.298,17.931 9.401,17.98C13.802,18.175 17.202,16.958 20.653,13.725C22.214,12.263 23.781,10.43 25.071,8.796L20.395,9.536C19.305,9.709 18.28,8.965 18.107,7.874C17.935,6.784 18.679,5.759 19.769,5.586L29.259,4.083L29.46,4.062Z"/>
|
||||
|
||||
</vector>
|
||||
12
core/ui/src/main/res/drawable/ic_explore_16.xml
Normal file
12
core/ui/src/main/res/drawable/ic_explore_16.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M2.667,8C2.667,5.055 5.055,2.667 8,2.667C10.946,2.667 13.333,5.055 13.333,8C13.333,10.946 10.946,13.333 8,13.333C5.055,13.333 2.667,10.946 2.667,8ZM1.333,8C1.333,11.682 4.318,14.667 8,14.667C11.682,14.667 14.667,11.682 14.667,8C14.667,4.318 11.682,1.334 8,1.334C4.318,1.334 1.333,4.318 1.333,8Z"
|
||||
android:fillColor="#919191"/>
|
||||
<path
|
||||
android:pathData="M5.032,11.297C4.984,11.287 4.94,11.263 4.906,11.229C4.871,11.194 4.847,11.15 4.837,11.102C4.827,11.054 4.832,11.005 4.85,10.959L6.315,7.297C6.416,7.045 6.567,6.816 6.758,6.625C6.95,6.433 7.178,6.282 7.429,6.182L11.092,4.717C11.137,4.699 11.188,4.695 11.235,4.704C11.283,4.714 11.328,4.738 11.362,4.772C11.396,4.807 11.42,4.851 11.43,4.899C11.44,4.947 11.435,4.997 11.417,5.042L9.952,8.704C9.851,8.956 9.7,9.185 9.509,9.376C9.317,9.568 9.089,9.718 8.838,9.819L5.175,11.284C5.13,11.302 5.08,11.306 5.032,11.297ZM8.28,8.736C8.425,8.707 8.559,8.636 8.664,8.531C8.768,8.426 8.84,8.292 8.869,8.147C8.898,8.001 8.884,7.85 8.827,7.713C8.77,7.576 8.673,7.46 8.55,7.377C8.427,7.295 8.282,7.251 8.134,7.251C7.935,7.251 7.744,7.33 7.603,7.47C7.463,7.611 7.384,7.802 7.384,8.001C7.384,8.149 7.427,8.294 7.51,8.417C7.592,8.54 7.709,8.637 7.846,8.694C7.983,8.75 8.134,8.765 8.28,8.736Z"
|
||||
android:fillColor="#919191"/>
|
||||
</vector>
|
||||
15
core/ui/src/main/res/drawable/ic_heart_20.xml
Normal file
15
core/ui/src/main/res/drawable/ic_heart_20.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h20v20h-20z"/>
|
||||
<path
|
||||
android:pathData="M13.75,1.75C16.73,1.75 19.083,4.093 19.084,7.083C19.084,8.9 18.257,10.495 16.961,12.081C15.673,13.657 13.843,15.315 11.713,17.247L10.505,18.347L10,18.806L9.495,18.347L8.288,17.247C6.157,15.315 4.327,13.657 3.039,12.081C1.742,10.495 0.917,8.9 0.917,7.083C0.917,4.093 3.27,1.75 6.25,1.75C7.646,1.75 8.986,2.29 10,3.174C11.013,2.29 12.353,1.75 13.75,1.75Z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#1E1E1E"/>
|
||||
</group>
|
||||
</vector>
|
||||
15
core/ui/src/main/res/drawable/ic_quick_recap_16.xml
Normal file
15
core/ui/src/main/res/drawable/ic_quick_recap_16.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M1.873,2.461C2.205,2.461 2.473,2.73 2.473,3.061V8.464C2.473,9.9 3.638,11.064 5.073,11.064H11.855V10.146C11.855,9.831 12.203,9.639 12.469,9.809L14.858,11.326C15.105,11.483 15.105,11.844 14.858,12.002L12.469,13.519C12.203,13.688 11.855,13.497 11.855,13.181V12.264H5.073C2.975,12.264 1.273,10.562 1.273,8.464V3.061C1.273,2.73 1.542,2.461 1.873,2.461Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
<path
|
||||
android:pathData="M9.473,6.413C9.805,6.413 10.073,6.682 10.073,7.013C10.073,7.345 9.805,7.613 9.473,7.613H5.673C5.342,7.613 5.074,7.345 5.073,7.013C5.073,6.682 5.342,6.413 5.673,6.413H9.473Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
<path
|
||||
android:pathData="M13.273,2.696C13.605,2.696 13.873,2.964 13.873,3.296C13.873,3.627 13.605,3.896 13.273,3.896H5.673C5.342,3.896 5.074,3.627 5.073,3.296C5.073,2.964 5.342,2.696 5.673,2.696H13.273Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
</vector>
|
||||
9
core/ui/src/main/res/drawable/ic_tangem_pay_24.xml
Normal file
9
core/ui/src/main/res/drawable/ic_tangem_pay_24.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<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="M18.327,4.365C20.007,4.365 20.847,4.365 21.489,4.692C22.053,4.98 22.512,5.439 22.8,6.003C23.127,6.645 23.127,7.485 23.127,9.165V14.836C23.127,16.516 23.127,17.356 22.8,17.998C22.512,18.562 22.053,19.021 21.489,19.309C20.847,19.636 20.007,19.636 18.327,19.636H5.675C3.995,19.636 3.154,19.636 2.513,19.309C1.948,19.021 1.49,18.562 1.202,17.998C0.875,17.356 0.875,16.516 0.875,14.836V9.165C0.875,7.485 0.875,6.645 1.202,6.003C1.49,5.439 1.948,4.98 2.513,4.692C3.154,4.365 3.995,4.365 5.675,4.365H18.327ZM14.403,9.195C13.006,9.195 11.988,9.949 11.988,10.995C11.988,11.79 12.708,12.208 13.217,12.46C13.767,12.711 13.979,12.878 13.936,13.129C13.936,13.505 13.513,13.673 13.09,13.673C12.581,13.673 12.073,13.547 11.607,13.338L11.354,14.51C11.862,14.719 12.412,14.803 12.921,14.803C14.488,14.844 15.462,14.092 15.462,12.962C15.462,11.539 13.471,11.455 13.471,10.828C13.513,10.535 13.767,10.367 14.064,10.367C14.53,10.325 15.038,10.409 15.462,10.618L15.716,9.447C15.292,9.28 14.826,9.195 14.403,9.195ZM2.881,9.321V9.489C3.389,9.573 3.855,9.74 4.278,9.949C4.448,10.033 4.563,10.21 4.617,10.41L5.761,14.762H7.285L9.572,9.321H8.09L6.565,13.004L5.973,9.865C5.93,9.573 5.676,9.321 5.337,9.321H2.881ZM10.164,9.321L8.979,14.762H10.419L11.604,9.321H10.164ZM18.13,9.321C17.876,9.321 17.622,9.489 17.537,9.74L15.42,14.762H16.902L17.198,13.967H19.02L19.188,14.762H20.502L19.358,9.321H18.13ZM18.76,12.838H17.574L18.337,10.787L18.76,12.838Z"
|
||||
android:fillColor="#656565"/>
|
||||
</vector>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -0,0 +1,155 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DateUtilsTest {
|
||||
|
||||
private lateinit var defaultTimeZone: DateTimeZone
|
||||
|
||||
private val now = createDateTime(day = 14, hour = 12)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
defaultTimeZone = DateTimeZone.getDefault()
|
||||
DateTimeZone.setDefault(DateTimeZone.forID("Europe/Moscow"))
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
DateTimeZone.setDefault(defaultTimeZone)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return MinutesAgo when difference is less than 1 minute`() {
|
||||
val pastDate = now.minusSeconds(30)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return MinutesAgo when difference is 1 minute`() {
|
||||
val pastDate = now.minusMinutes(1)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return MinutesAgo when difference is 30 minutes`() {
|
||||
val pastDate = now.minusMinutes(30)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(30)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return MinutesAgo when difference is 59 minutes`() {
|
||||
val pastDate = now.minusMinutes(59).minusSeconds(59)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(59)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return HoursAgo when difference is exactly 1 hour`() {
|
||||
val pastDate = now.minusHours(1)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.HoursAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.HoursAgo).hours).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return HoursAgo when difference is 11 hours`() {
|
||||
val pastDate = now.minusHours(11)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.HoursAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.HoursAgo).hours).isEqualTo(11)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return Today when difference is exactly 12 hours`() {
|
||||
val pastDate = createDateTime(day = 14, hour = 0)
|
||||
val createdAt = pastDate.toString()
|
||||
val nowInTest = createDateTime(day = 14, hour = 12)
|
||||
val result = getFormattedDate(createdAt, nowInTest)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.Today::class.java)
|
||||
Truth.assertThat((result as FormattedDate.Today).time).isEqualTo("03:00")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return FullDate when difference is 18 hours and past date is another day`() {
|
||||
val pastDate = createDateTime(day = 13, hour = 18)
|
||||
val createdAt = pastDate.toString()
|
||||
val nowInTest = createDateTime(day = 14, hour = 12)
|
||||
val result = getFormattedDate(createdAt, nowInTest)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return FullDate when difference is exactly 24 hours`() {
|
||||
val pastDate = now.minusDays(1)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return FullDate when difference is 2 days`() {
|
||||
val pastDate = now.minusDays(2)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return FullDate with original string when format has wrong date separator`() {
|
||||
val wrongFormat = "2025/10/14T12:00:00.000Z"
|
||||
val result = getFormattedDate(wrongFormat, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java)
|
||||
Truth.assertThat((result as FormattedDate.FullDate).date).isEqualTo(wrongFormat)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle future dates correctly`() {
|
||||
val futureDate = now.plusHours(1)
|
||||
val createdAt = futureDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle edge case of exactly 0 milliseconds difference`() {
|
||||
val createdAt = now.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1)
|
||||
}
|
||||
|
||||
private fun createDateTime(day: Int, hour: Int): DateTime {
|
||||
return DateTime(
|
||||
/* year = */ 2025,
|
||||
/* monthOfYear = */ 10,
|
||||
/* dayOfMonth = */ day,
|
||||
/* hourOfDay = */ hour,
|
||||
/* minuteOfHour = */ 0,
|
||||
/* secondOfMinute = */ 0,
|
||||
/* millisOfSecond = */ 0,
|
||||
/* zone = */ DateTimeZone.UTC,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue