Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-29 21:44:25 +03:00
commit bf0cb06cb3
297 changed files with 4771 additions and 2376 deletions

View file

@ -1,121 +1,20 @@
package com.tangem.core.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.states.Item
import com.tangem.core.ui.components.states.SelectableItemsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.valentinilk.shimmer.shimmer
import kotlinx.collections.immutable.toImmutableList
/**
* Small card with text information attached to the edges
*
* @param startText text information attached to the left edge
* @param endText text information attached to the right edge
* @param isLoading if true, shimmer is shown instead of [startText] and [endText]
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=290%3A339&t=WdN5XpixzZLlQAZO-4"
* >Figma component</a>
*/
@Composable
fun SmallInfoCard(startText: String, endText: String, isLoading: Boolean = false) {
Surface(
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
color = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
) {
CardInfoBox(
startText = startText,
endText = endText,
isLoading = isLoading,
disclaimer = null,
)
}
}
/**
* Small card with text information attached to the edges
*
* @param startText text information attached to the left edge
* @param endText text information attached to the right edge
* @param disclaimer closable description text
* @param isLoading if true, shimmer is shown instead of [startText] and [endText]
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1007%3A709&t=X19ZWlYP0rnmdcDO-4"
* >Figma component</a>
*/
@Composable
fun SmallInfoCardWithDisclaimer(startText: String, endText: String, disclaimer: String, isLoading: Boolean = false) {
Surface(
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
color = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
) {
CardInfoBox(
startText = startText,
endText = endText,
isLoading = isLoading,
disclaimer = disclaimer,
)
}
}
/**
* [SmallInfoCard] with warning information shown underneath it.
*
* @param startText text information attached to the left edge
* @param endText text information attached to the right edge
* @param warningText text of the warning
* @param disclaimer closable description text
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=515%3A1541&t=qYGQJdtf1eeUPdkR-1"
* >Figma component</a>
*/
@Composable
fun SmallInfoCardWithWarning(startText: String, endText: String, warningText: String, disclaimer: String? = null) {
Surface(
shape = RoundedCornerShape(TangemTheme.dimens.size12),
color = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
CardInfoBox(startText = startText, endText = endText, disclaimer = disclaimer)
Divider(
color = TangemTheme.colors.stroke.primary,
thickness = TangemTheme.dimens.size0_5,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),
)
WarningItem(warningText = warningText)
}
}
}
/**
* Small card with an icon to the left and title with description shown to the right of it.
@ -140,7 +39,8 @@ fun CardWithIcon(
Surface(
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
color = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
tonalElevation = TangemTheme.dimens.elevation2,
shadowElevation = TangemTheme.dimens.elevation2,
) {
IconWithTitleAndDescription(
title = title,
@ -223,393 +123,12 @@ internal fun IconWithTitleAndDescription(
}
}
/**
* Card with text information attached to the edges and selectable items
*
* @param state state of block with items
* @param isLoading if true, shimmer is shown instead of data
* @param disclaimer description text
* @param onSelect listener on item selection
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=290-217&t=FMnRqkWPAgdZSdhv-0"
* >Figma component</a>
*/
@Composable
fun <T> SelectableInfoCard(
state: SelectableItemsState<T>,
onSelect: (Item<T>) -> Unit,
isLoading: Boolean = false,
disclaimer: String? = null,
) {
Surface(
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
color = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
) {
SelectableInfoBlock(
state = state,
isLoading = isLoading,
disclaimer = disclaimer,
onSelect = onSelect,
)
}
}
/**
* Card with text information attached to the edges, selectable items and warning
*
* @param state state of block with items
* @param isLoading if true, shimmer is shown instead of data
* @param disclaimer description text
* @param onSelect listener on item selection
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=290-217&t=FMnRqkWPAgdZSdhv-0"
* >Figma component</a>
*/
@Composable
fun <T> SelectableInfoCardWithWarning(
state: SelectableItemsState<T>,
warningText: String,
onSelect: (Item<T>) -> Unit,
isLoading: Boolean = false,
disclaimer: String? = null,
) {
Surface(
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
color = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
) {
SelectableInfoBlock(
state = state,
isLoading = isLoading,
disclaimer = disclaimer,
warningText = warningText,
onSelect = onSelect,
)
}
}
// region elements
@Composable
private fun CardInfoBox(startText: String, endText: String, disclaimer: String?, isLoading: Boolean = false) {
val isOpened = remember { mutableStateOf(false) }
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(TangemTheme.dimens.size48)
.clickable { isOpened.value = !isOpened.value }
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = startText,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
style = TangemTheme.typography.subtitle2,
)
if (isLoading) {
ShimmerItem(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12)
} else {
Row {
Text(
text = endText,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.body2,
)
if (disclaimer != null) {
val chevronIcon = if (isOpened.value) {
painterResource(id = R.drawable.ic_chevron_up_24)
} else {
painterResource(id = R.drawable.ic_chevron_24)
}
Icon(
modifier = Modifier.size(TangemTheme.dimens.size20),
painter = chevronIcon,
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
}
}
}
}
if (isOpened.value && disclaimer != null) {
DisclaimerItem(disclaimer)
}
}
}
@Suppress("LongMethod")
@Composable
private fun <T> SelectableInfoBlock(
state: SelectableItemsState<T>,
onSelect: (Item<T>) -> Unit,
isLoading: Boolean = false,
disclaimer: String? = null,
warningText: String? = null,
) {
var isOpened by remember { mutableStateOf(false) }
Column {
if (isLoading) {
ShimmerItem(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12)
} else {
val selectedItem = state.selectedItem
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(TangemTheme.dimens.size48)
.clickable { isOpened = !isOpened }
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = selectedItem.startText.resolveReference(),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
style = TangemTheme.typography.subtitle2,
)
Row {
Text(
text = selectedItem.endText.resolveReference(),
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.body2,
)
val chevronIcon = if (isOpened) {
painterResource(id = R.drawable.ic_chevron_up_24)
} else {
painterResource(id = R.drawable.ic_chevron_24)
}
Icon(
modifier = Modifier.size(TangemTheme.dimens.size20),
painter = chevronIcon,
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
}
}
Divider(
color = TangemTheme.colors.stroke.primary,
thickness = TangemTheme.dimens.size0_5,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),
)
if (isOpened) {
state.items.forEach { item ->
SelectableItem(item) {
onSelect.invoke(item)
}
Divider(
color = TangemTheme.colors.stroke.primary,
thickness = TangemTheme.dimens.size0_5,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),
)
}
if (disclaimer != null) {
DisclaimerItem(disclaimer)
Divider(
color = TangemTheme.colors.stroke.primary,
thickness = TangemTheme.dimens.size0_5,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),
)
}
}
if (warningText?.isNotEmpty() == true) {
WarningItem(warningText = warningText)
}
}
}
}
@Composable
private fun <T> SelectableItem(item: Item<T>, onSelect: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(TangemTheme.dimens.size48)
.clickable { onSelect() }
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = item.startText.resolveReference(),
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.subtitle2,
)
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = item.endText.resolveReference(),
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.body2,
)
val size = TangemTheme.dimens.size24
if (item.isSelected) {
Icon(
modifier = Modifier.size(size),
painter = painterResource(id = R.drawable.ic_check_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
} else {
SpacerW(width = size)
}
}
}
}
@Composable
private fun ShimmerItem(width: Dp, height: Dp) {
Box(
modifier = Modifier
.size(width = width, height = height)
.shimmer()
.background(
color = TangemTheme.colors.button.secondary,
shape = RoundedCornerShape(TangemTheme.dimens.radius3),
),
)
}
@Composable
private fun DisclaimerItem(disclaimer: String) {
Divider(
color = TangemTheme.colors.stroke.primary,
thickness = TangemTheme.dimens.size0_5,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = TangemTheme.dimens.spacing14,
bottom = TangemTheme.dimens.spacing16,
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.secondary,
shape = CircleShape,
)
.height(TangemTheme.dimens.size40)
.width(TangemTheme.dimens.size40),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_heart_24),
tint = TangemTheme.colors.icon.secondary,
contentDescription = null,
)
}
SpacerW12()
Text(
text = disclaimer,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
)
}
}
@Composable
private fun WarningItem(warningText: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = TangemTheme.dimens.spacing14,
bottom = TangemTheme.dimens.spacing16,
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.secondary,
shape = CircleShape,
)
.height(TangemTheme.dimens.size40)
.width(TangemTheme.dimens.size40),
contentAlignment = Alignment.Center,
) {
Image(
painter = painterResource(id = R.drawable.img_attention_20),
contentDescription = null,
)
}
SpacerW12()
Text(
text = warningText,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body2,
)
}
}
// endregion elements
// region Preview
@Suppress("MagicNumber")
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun CardsPreview() {
Column(modifier = Modifier.fillMaxWidth()) {
SmallInfoCard(
startText = "Balance",
endText = "0.4405434 BTC",
)
SpacerH32()
SmallInfoCardWithDisclaimer(
startText = "Balance",
endText = "0.4405434 BTC",
disclaimer = "Not enough funds for fee on your Polygon wallet to create a transaction." +
"Top up your Polygon wallet first.",
)
SpacerH32()
SmallInfoCardWithWarning(
startText = "Balance",
endText = "0.4405434 BTC",
warningText = "Not enough funds for fee on your Polygon wallet to create a transaction. " +
"Top up your Polygon wallet first.",
disclaimer = "Not enough funds for fee on your Polygon wallet to create a transaction." +
"Top up your Polygon wallet first.",
)
SpacerH32()
private fun Preview_CardWithIcon() {
TangemThemePreview {
CardWithIcon(
title = "Permit is valid until",
description = "26:30",
@ -621,52 +140,6 @@ private fun CardsPreview() {
)
},
)
SpacerH32()
val state = SelectableItemsState(
selectedItem = Item(0, TextReference.Str("Balance"), TextReference.Str("0.4405434 BTC"), true, ""),
items = listOf(
Item(0, TextReference.Str("Normal"), TextReference.Str("0.4405434 BTC"), true, ""),
Item(1, TextReference.Str("Priority"), TextReference.Str("0.46 BTC"), false, ""),
).toImmutableList(),
)
SelectableInfoCard(
state = state,
isLoading = false,
disclaimer = "Not enough funds for fee on your Polygon wallet to create a transaction",
onSelect = {},
)
SpacerH32()
SelectableInfoCardWithWarning(
state = state,
isLoading = false,
warningText = "Not enough funds for fee on your Polygon wallet to create a transaction. " +
"Top up your Polygon wallet first.",
disclaimer = "Not enough funds for fee on your Polygon wallet to create a transaction",
onSelect = {},
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_Cards() {
TangemThemePreview {
CardsPreview()
}
}
@Preview(widthDp = 328, heightDp = 48, showBackground = true)
@Preview(widthDp = 328, heightDp = 48, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_SimpleInfoCard() {
TangemThemePreview {
SmallInfoCard(startText = "Balance", endText = "0.4405434 BTC")
SmallInfoCardWithDisclaimer(startText = "Balance", endText = "0.4405434 BTC", disclaimer = "test")
}
}
// endregion Preview

View file

@ -1,209 +0,0 @@
package com.tangem.core.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
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.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
/**
* A card with a warning icon to the left and title with description shown to the right of it.
*
* @param title title of the warning in bold
* @param description description of the warning
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=290%3A217&t=yMepJZTRh5bLkOoJ-1"
* >Figma component</a>
*/
@Composable
fun WarningCard(title: String, description: String, icon: @Composable (() -> Unit)? = null) {
WarningCardSurface(
content = {
WarningBody(
title = title,
description = description,
icon = icon,
)
},
)
}
/**
* A card with a warning icon to the left and title without description shown to the right of it.
*
* @param title title of the warning in bold
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=290%3A217&t=yMepJZTRh5bLkOoJ-1"
* >Figma component</a>
*/
@Composable
fun WarningCardTitleOnly(title: String, icon: @Composable (() -> Unit)? = null) {
WarningCardMaterial3Style(
content = {
WarningBody(
title = title,
description = null,
icon = icon,
iconBackground = TangemTheme.colors.button.disabled,
)
},
)
}
/**
* [WarningCard], but clickable (with an 'greater then' icon to the left)
*
* @param title title of the warning in bold
* @param description description of the warning
* @param onClick action to be performed when warning is clicked
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=402%3A528&t=yMepJZTRh5bLkOoJ-1"
* >Figma component</a>
*/
@Composable
fun ClickableWarningCard(
title: String,
description: String,
icon: @Composable (() -> Unit)? = null,
onClick: () -> Unit,
) {
WarningCardSurface(
content = {
WarningBody(title = title, description = description, icon = icon) {
SpacerW12()
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
},
onClick = onClick,
)
}
/**
* [WarningCard], but clickable and with a 'refresh' icon at the right edge
*
* @param title title of the warning in bold
* @param description description of the warning
* @param onClick action to be performed when warning is clicked
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=290%3A217&t=yMepJZTRh5bLkOoJ-1"
* >Figma component</a>
*/
@Composable
fun RefreshableWarningCard(
title: String,
description: String,
icon: @Composable (() -> Unit)? = null,
onClick: () -> Unit,
) {
WarningCardSurface(
content = {
WarningBody(title = title, description = description, icon = icon) {
SpacerW12()
Icon(
painter = painterResource(id = R.drawable.ic_refresh_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
}
},
onClick = onClick,
)
}
// region elements
@Composable
private fun WarningBody(
title: String,
description: String?,
iconBackground: Color = TangemTheme.colors.background.secondary,
icon: @Composable (() -> Unit)? = null,
additionalContent: @Composable () -> Unit = {},
) {
IconWithTitleAndDescription(
title = title,
description = description,
additionalContent = additionalContent,
icon = icon ?: {
Image(
painter = painterResource(id = R.drawable.img_attention_20),
contentDescription = null,
)
},
iconBackground = iconBackground,
)
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun WarningCardSurface(onClick: (() -> Unit)? = null, content: @Composable () -> Unit) {
Card(
shape = RoundedCornerShape(TangemTheme.dimens.size12),
backgroundColor = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
onClick = onClick ?: {},
enabled = onClick != null,
) {
content()
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun WarningCardMaterial3Style(onClick: (() -> Unit)? = null, content: @Composable () -> Unit) {
Card(
shape = RoundedCornerShape(TangemTheme.dimens.radius16),
backgroundColor = TangemTheme.colors.button.disabled,
elevation = TangemTheme.dimens.elevation0,
onClick = onClick ?: {},
enabled = onClick != null,
) {
content()
}
}
// endregion elements
// region Preview
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun WarningsPreview() {
TangemThemePreview {
Column(modifier = Modifier.fillMaxWidth()) {
WarningCard(
title = "Exchange rate has expired",
description = "To access all the networks, you need to scan the card.",
)
SpacerH32()
ClickableWarningCard(
title = "Exchange rate has expired",
description = "To access all the networks, you need to scan the card.",
onClick = {},
)
SpacerH32()
RefreshableWarningCard(
title = "Exchange rate has expired",
description = "To access all the networks, you need to scan the card.",
onClick = {},
)
}
}
}
// endregion Preview

View file

@ -1,4 +1,4 @@
package com.tangem.core.ui.pullToRefresh
package com.tangem.core.ui.components.containers.pullToRefresh
/**
* Pull to refresh config data

View file

@ -0,0 +1,63 @@
package com.tangem.core.ui.components.containers.pullToRefresh
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TangemPullToRefreshContainer(
config: PullToRefreshConfig,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
val state = rememberPullToRefreshState()
PullToRefreshBox(
isRefreshing = config.isRefreshing,
onRefresh = {
config.onRefresh(PullToRefreshConfig.ShowRefreshState())
},
state = state,
modifier = modifier,
indicator = {
Indicator(
modifier = Modifier.align(Alignment.TopCenter),
isRefreshing = config.isRefreshing,
state = state,
containerColor = TangemTheme.colors.background.tertiary,
color = TangemTheme.colors.text.primary1,
)
},
) {
content()
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360, heightDp = 720)
@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPullToRefreshContainer_Preview() {
TangemThemePreview {
TangemPullToRefreshContainer(
config = PullToRefreshConfig(isRefreshing = true, {}),
) {
Box(
modifier = Modifier.fillMaxSize()
.background(TangemTheme.colors.background.secondary),
)
}
}
}
// endregion

View file

@ -1,206 +0,0 @@
package com.tangem.core.ui.components.notifications
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import androidx.constraintlayout.compose.Visibility
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.TangemColorPalette.Dark6
import com.tangem.core.ui.res.TangemColorPalette.Light4
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Custom notification with image background
* @see [Swap Promo](https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=8713-6307&mode=dev)
*/
@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries")
@Composable
fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = Modifier) {
val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig
ConstraintLayout(
modifier = modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size62)
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.clickable(
enabled = config.onClick != null,
onClick = config.onClick ?: {},
),
) {
val (iconRef, titleRef, subtitleRef, closeIconRef, buttonRef, backgroundRef) = createRefs()
val spacing2 = TangemTheme.dimens.spacing2
val spacing12 = TangemTheme.dimens.spacing12
val spacing14 = TangemTheme.dimens.spacing14
Image(
painter = painterResource(config.backgroundResId ?: R.drawable.img_swap_promo_blue_banner_background),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.constrainAs(backgroundRef) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
visibility = if (config.backgroundResId == null) Visibility.Gone else Visibility.Visible
},
)
Image(
painter = painterResource(config.iconResId),
contentDescription = null,
modifier = Modifier
.size(TangemTheme.dimens.size34)
.constrainAs(iconRef) {
start.linkTo(parent.start, spacing12)
linkTo(titleRef.top, subtitleRef.bottom, bias = 0.1f)
},
)
val titleText = config.title?.resolveReference()
if (titleText != null) {
Text(
text = titleText,
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.constantWhite,
modifier = Modifier.constrainAs(titleRef) {
top.linkTo(parent.top, spacing12)
start.linkTo(iconRef.end, spacing12)
end.linkTo(closeIconRef.start, spacing2)
width = Dimension.fillToConstraints
},
)
}
Text(
text = config.subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.constantWhite,
modifier = Modifier.constrainAs(subtitleRef) {
top.linkTo(titleRef.bottom, spacing2)
start.linkTo(iconRef.end, spacing12)
end.linkTo(parent.end, spacing12)
bottom.linkTo(buttonRef.top, spacing12, spacing14)
width = Dimension.fillToConstraints
},
)
Icon(
painter = painterResource(id = R.drawable.ic_close_24),
contentDescription = null,
tint = TangemTheme.colors.text.constantWhite,
modifier = Modifier
.size(TangemTheme.dimens.size16)
.constrainAs(closeIconRef) {
top.linkTo(parent.top, spacing12)
end.linkTo(parent.end, spacing12)
}
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
) {
config.onCloseClick?.invoke()
},
)
val isDarkMode = LocalIsInDarkTheme.current
TangemButton(
modifier = Modifier.constrainAs(buttonRef) {
start.linkTo(parent.start, spacing12)
end.linkTo(parent.end, spacing12)
bottom.linkTo(parent.bottom, spacing12)
width = Dimension.fillToConstraints
visibility = if (button == null) {
Visibility.Gone
} else {
Visibility.Visible
}
},
text = button?.text?.resolveReference().orEmpty(),
icon = TangemButtonIconPosition.Start(button?.iconResId ?: R.drawable.ic_exchange_vertical_24),
onClick = button?.onClick ?: {},
colors = ButtonColors(
containerColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary,
contentColor = Dark6,
disabledContainerColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
),
textStyle = TangemTheme.typography.subtitle1,
enabled = true,
showProgress = false,
)
}
}
//region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun NotificationWithBackgroundPreview(
@PreviewParameter(NotificationWithBackgroundPreviewProvider::class) config: NotificationConfig,
) {
TangemThemePreview {
NotificationWithBackground(config = config)
}
}
private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvider<NotificationConfig> {
override val values: Sequence<NotificationConfig>
get() = sequenceOf(
NotificationConfig(
title = resourceReference(id = R.string.main_swap_changelly_promotion_title),
subtitle = resourceReference(
id = R.string.main_swap_changelly_promotion_message,
formatArgs = wrappedList("1", "2", "3"),
),
iconResId = R.drawable.img_swap_promo,
backgroundResId = R.drawable.img_swap_promo_blue_banner_background,
),
NotificationConfig(
title = resourceReference(
id = R.string.token_swap_changelly_promotion_title,
formatArgs = wrappedList("1"),
),
subtitle = stringReference(
"Swap multiple currencies between any chains you wish. Swap multiple " +
"currencies between any chains you wish. Swap multiple " +
"currencies between any chains you wish. Commission free period " +
"till Dec 31.",
),
iconResId = R.drawable.img_swap_promo,
backgroundResId = R.drawable.img_swap_promo_blue_banner_background,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(id = R.string.token_swap_promotion_button),
onClick = {},
),
),
)
}
//endregion

View file

@ -1,169 +0,0 @@
package com.tangem.core.ui.components.notifications
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.TangemColorPalette.Dark6
import com.tangem.core.ui.res.TangemColorPalette.Light4
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
private val OxkPromoColor = Color(0xFFBCFF2F)
@Composable
fun OkxPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) {
Column(
modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(Dark6),
) {
Content(config = config)
Button(config = config)
}
}
@Composable
private fun Content(config: NotificationConfig) {
Row {
Icon(
painter = rememberVectorPainter(image = ImageVector.vectorResource(id = R.drawable.img_okx_dex_logo)),
contentDescription = null,
tint = TangemTheme.colors.icon.constant,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12)
.align(Alignment.CenterVertically),
)
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
modifier = Modifier
.weight(1f)
.padding(TangemTheme.dimens.spacing12),
) {
val titleText = config.title?.resolveReference()
if (titleText != null) {
Text(
text = titleText,
style = TangemTheme.typography.button,
color = OxkPromoColor,
)
}
Text(
text = config.subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.constantWhite,
)
}
config.onCloseClick?.let {
Icon(
painter = rememberVectorPainter(
image = ImageVector.vectorResource(R.drawable.ic_close_24),
),
contentDescription = null,
tint = TangemTheme.colors.icon.constant,
modifier =
Modifier
.padding(
top = TangemTheme.dimens.spacing8,
end = TangemTheme.dimens.spacing8,
)
.size(TangemTheme.dimens.size20)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(radius = TangemTheme.dimens.radius10),
onClick = it,
),
)
}
}
}
@Composable
private fun Button(config: NotificationConfig) {
val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig
button?.let {
val isDarkMode = LocalIsInDarkTheme.current
TangemButton(
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
text = button.text.resolveReference(),
icon = TangemButtonIconPosition.Start(button.iconResId ?: R.drawable.ic_exchange_vertical_24),
onClick = button.onClick,
colors = ButtonColors(
containerColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary,
contentColor = Dark6,
disabledContainerColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
),
textStyle = TangemTheme.typography.subtitle1,
enabled = true,
showProgress = false,
)
}
}
// region Preview
@Preview(widthDp = 360)
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun OkxPromoNotification_Preview(
@PreviewParameter(OkxPromoNotificationPreviewProvider::class) data: NotificationConfig,
) {
TangemThemePreview {
OkxPromoNotification(data)
}
}
private class OkxPromoNotificationPreviewProvider : PreviewParameterProvider<NotificationConfig> {
override val values: Sequence<NotificationConfig>
get() = sequenceOf(
NotificationConfig(
title = resourceReference(R.string.swap_promo_title),
subtitle = resourceReference(R.string.swap_promo_text),
iconResId = R.drawable.img_okx_dex_logo,
onCloseClick = {},
),
NotificationConfig(
title = resourceReference(R.string.swap_promo_title),
subtitle = resourceReference(R.string.swap_promo_text),
iconResId = R.drawable.img_okx_dex_logo,
onCloseClick = {},
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.token_swap_promotion_button),
onClick = {},
),
),
)
}
// endregion

View file

@ -1,131 +0,0 @@
package com.tangem.core.ui.components.notifications
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.FixedScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
fun RingPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) {
var textHeightDp by remember { mutableStateOf(0.dp) }
Box(
modifier = modifier
.fillMaxWidth()
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
.background(TangemColorPalette.Dark6),
) {
PromoImage(
backgroundRes = config.backgroundResId,
iconRes = config.iconResId,
modifier = Modifier.height(textHeightDp),
)
PromoText(
title = config.title,
subtitle = config.subtitle,
onSizeChange = { textHeightDp = it },
)
CloseableIconButton(
onClick = config.onCloseClick,
modifier = Modifier.align(alignment = Alignment.TopEnd),
iconTint = TangemTheme.colors.icon.informative,
)
}
}
@Composable
private fun PromoImage(@DrawableRes backgroundRes: Int?, @DrawableRes iconRes: Int, modifier: Modifier = Modifier) {
if (backgroundRes == null) return
ConstraintLayout(
modifier = modifier,
) {
val (background, image) = createRefs()
Image(
painter = painterResource(backgroundRes),
contentDescription = null,
contentScale = FixedScale(2f),
modifier = Modifier
.constrainAs(background) {
top.linkTo(parent.top)
start.linkTo(image.start)
end.linkTo(image.end)
},
)
Image(
painter = painterResource(id = iconRes),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.requiredWidth(width = 60.dp)
.wrapContentHeight(Alignment.Top, unbounded = true)
.constrainAs(image) {
top.linkTo(parent.top, 11.dp)
start.linkTo(parent.start, 9.dp)
},
)
}
}
@Composable
private fun PromoText(title: TextReference?, subtitle: TextReference, onSizeChange: (Dp) -> Unit) {
val density = LocalDensity.current
Box(
modifier = Modifier.onSizeChanged {
with(density) { onSizeChange(it.height.toDp()) }
},
) {
TextsBlock(
title = title,
subtitle = subtitle,
subtitleColor = TangemTheme.colors.text.tertiary,
titleColor = TangemTheme.colors.text.constantWhite,
modifier = Modifier
.wrapContentHeight()
.align(Alignment.CenterStart)
.padding(start = 80.dp, top = 12.dp, end = 12.dp, bottom = 12.dp),
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, locale = "fr")
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun RingPromoNotification_Preview() {
TangemThemePreview {
RingPromoNotification(
NotificationConfig(
title = resourceReference(id = R.string.ring_promo_title),
subtitle = resourceReference(id = R.string.ring_promo_text),
iconResId = R.drawable.img_card_with_ring,
backgroundResId = R.drawable.img_ring_promo_background,
onCloseClick = { },
),
)
}
}
// endregion

View file

@ -3,11 +3,11 @@
package com.tangem.core.ui.components.progressbar
import android.content.res.Configuration
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
@ -39,7 +39,7 @@ import kotlin.math.abs
* @param strokeCap stroke cap to use for the ends of this progress indicator
*/
@Composable
fun LinearProgressIndicator(
fun TangemLinearProgressIndicator(
progress: () -> Float,
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.text.accent,
@ -61,6 +61,99 @@ fun LinearProgressIndicator(
}
}
/**
* Progress indicators loading status with infinite animation
*
* @param modifier the [Modifier] to be applied to this progress indicator
* @param color The color of the progress indicator.
* @param backgroundColor The color of the background behind the indicator, visible when the
* progress has not reached that area of the overall indicator yet.
* @param strokeCap stroke cap to use for the ends of this progress indicator
*/
@Suppress("LongMethod")
@Composable
fun TangemLinearProgressIndicator(
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.text.accent,
backgroundColor: Color = TangemTheme.colors.background.tertiary,
strokeCap: StrokeCap = StrokeCap.Round,
) {
val infiniteTransition = rememberInfiniteTransition()
val firstLineHead by infiniteTransition.animateFloat(
0f,
1f,
infiniteRepeatable(
animation = keyframes {
durationMillis = LINEAR_ANIMATION_DURATION
0f at FIRST_LINE_HEAD_DELAY using FIRST_LINE_HEAD_EASING
1f at FIRST_LINE_HEAD_DURATION + FIRST_LINE_HEAD_DELAY
},
),
)
val firstLineTail by infiniteTransition.animateFloat(
0f,
1f,
infiniteRepeatable(
animation = keyframes {
durationMillis = LINEAR_ANIMATION_DURATION
0f at FIRST_LINE_TAIL_DELAY using FIRST_LINE_TAIL_EASING
1f at FIRST_LINE_TAIL_DURATION + FIRST_LINE_TAIL_DELAY
},
),
)
val secondLineHead by infiniteTransition.animateFloat(
0f,
1f,
infiniteRepeatable(
animation = keyframes {
durationMillis = LINEAR_ANIMATION_DURATION
0f at SECOND_LINE_HEAD_DELAY using SECOND_LINE_HEAD_EASING
1f at SECOND_LINE_HEAD_DURATION + SECOND_LINE_HEAD_DELAY
},
),
)
val secondLineTail by infiniteTransition.animateFloat(
0f,
1f,
infiniteRepeatable(
animation = keyframes {
durationMillis = LINEAR_ANIMATION_DURATION
0f at SECOND_LINE_TAIL_DELAY using SECOND_LINE_TAIL_EASING
1f at SECOND_LINE_TAIL_DURATION + SECOND_LINE_TAIL_DELAY
},
),
)
Canvas(
modifier
.increaseSemanticsBounds()
.semantics(mergeDescendants = true) {
progressBarRangeInfo = ProgressBarRangeInfo.Indeterminate
}
.size(height = 4.dp, width = 24.dp),
) {
val strokeWidth = size.height
drawLinearIndicatorBackground(backgroundColor, strokeWidth, strokeCap)
if (firstLineHead - firstLineTail > 0) {
drawLinearIndicator(
firstLineHead,
firstLineTail,
color,
strokeWidth,
strokeCap,
)
}
if ((secondLineHead - secondLineTail) > 0) {
drawLinearIndicator(
secondLineHead,
secondLineTail,
color,
strokeWidth,
strokeCap,
)
}
}
}
internal fun Modifier.increaseSemanticsBounds(): Modifier {
val padding = 10.dp
return this
@ -129,16 +222,51 @@ private fun DrawScope.drawLinearIndicator(
private fun DrawScope.drawLinearIndicatorBackground(color: Color, strokeWidth: Float, strokeCap: StrokeCap) =
drawLinearIndicator(0f, 1f, color, strokeWidth, strokeCap)
// Indeterminate linear indicator transition specs
// Total duration for one cycle
private const val LINEAR_ANIMATION_DURATION = 1800
// Duration of the head and tail animations for both lines
private const val FIRST_LINE_HEAD_DURATION = 750
private const val FIRST_LINE_TAIL_DURATION = 850
private const val SECOND_LINE_HEAD_DURATION = 567
private const val SECOND_LINE_TAIL_DURATION = 533
// Delay before the start of the head and tail animations for both lines
private const val FIRST_LINE_HEAD_DELAY = 0
private const val FIRST_LINE_TAIL_DELAY = 333
private const val SECOND_LINE_HEAD_DELAY = 1000
private const val SECOND_LINE_TAIL_DELAY = 1267
private val FIRST_LINE_HEAD_EASING = CubicBezierEasing(0.2f, 0f, 0.8f, 1f)
private val FIRST_LINE_TAIL_EASING = CubicBezierEasing(0.4f, 0f, 1f, 1f)
private val SECOND_LINE_HEAD_EASING = CubicBezierEasing(0f, 0f, 0.65f, 1f)
private val SECOND_LINE_TAIL_EASING = CubicBezierEasing(0.1f, 0f, 0.45f, 1f)
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun LinearProgressIndicator_Preview() {
TangemThemePreview {
LinearProgressIndicator(
progress = { 0.5f },
modifier = Modifier.fillMaxWidth(),
)
Column(
modifier = Modifier
.background(TangemTheme.colors.background.secondary)
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
TangemLinearProgressIndicator(
progress = { 0.6f },
modifier = Modifier.fillMaxWidth(),
color = TangemTheme.colors.icon.primary1,
backgroundColor = TangemTheme.colors.background.tertiary,
)
TangemLinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
color = TangemTheme.colors.icon.primary1,
backgroundColor = TangemTheme.colors.background.tertiary,
)
}
}
}
// endregion

View file

@ -87,6 +87,8 @@ fun getActiveIconRes(blockchainId: String): Int {
"fact0rn" -> R.drawable.img_fact0rn_22
"odyssey", "odyssey/test" -> R.drawable.img_odyssey_chain_22
"bitrock", "bitrock/test" -> R.drawable.img_bitrock_22
"sonic", "sonic/test" -> R.drawable.img_sonic_22
"apechain", "apechain/test" -> R.drawable.img_apecoin_22
else -> R.drawable.ic_alert_24
}
}
@ -172,6 +174,8 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"fact0rn" -> R.drawable.img_fact0rn_22
"odyssey", "odyssey/test" -> R.drawable.img_odyssey_chain_22
"bitrock", "bitrock/test" -> R.drawable.img_bitrock_22
"sonic", "sonic/test" -> R.drawable.img_sonic_22
"apechain", "apechain/test" -> R.drawable.img_apecoin_22
else -> R.drawable.ic_alert_24
}
}
@ -260,6 +264,8 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"fact0rn" -> R.drawable.ic_fact0rn_22
"odyssey", "odyssey/test" -> R.drawable.ic_odyssey_chain_22
"bitrock", "bitrock/test" -> R.drawable.ic_bitrock_22
"sonic", "sonic/test" -> R.drawable.ic_sonic_22
"apechain", "apechain/test" -> R.drawable.ic_apecoin_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.core.ui.extensions
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isSimple
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
/**
* Modifier that adds an outset border to the content.
* @see [androidx.compose.foundation.border]
*
* @param width The width of the border.
* @param color The color of the border.
* @param shape The shape of the border.
*/
fun Modifier.outsetBorder(width: Dp, color: Color, shape: Shape) = drawWithCache {
val outline = shape.createOutline(size, layoutDirection, this)
val cornerRadius = when {
outline is Outline.Rounded && outline.roundRect.isSimple -> outline.roundRect.topLeftCornerRadius
else -> CornerRadius.Zero
}
onDrawWithContent {
drawContent()
drawRoundRect(
color = color,
topLeft = Offset(-width.toPx() / 2, -width.toPx() / 2),
size = size.copy(
width = size.width + width.toPx(),
height = size.height + width.toPx(),
),
cornerRadius = cornerRadius,
style = Stroke(width = width.toPx()),
)
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.core.ui.security
import android.view.WindowManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalContext
import com.tangem.core.ui.utils.findActivity
/**
* Disables screenshots for the current composition.
*/
@Composable
fun DisableScreenshotsDisposableEffect() {
val activity = LocalContext.current.findActivity()
DisposableEffect(activity) {
activity.window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
onDispose {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.core.ui.utils
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
error("Cannot find activity. Please make sure that the context is attached to an activity.")
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="8dp" android:viewportHeight="8" android:viewportWidth="32" android:width="32dp">
<path android:fillColor="#0099FF" android:pathData="M28,8C25.791,8 24,6.209 24,4C24,1.791 25.791,0 28,0C30.209,0 32,1.791 32,4C32,6.209 30.209,8 28,8Z"/>
<path android:fillAlpha="0.55" android:fillColor="#0099FF" android:pathData="M15,7C13.343,7 12,5.657 12,4C12,2.343 13.343,1 15,1C16.657,1 18,2.343 18,4C18,5.657 16.657,7 15,7Z" android:strokeAlpha="0.55"/>
<path android:fillAlpha="0.25" android:fillColor="#0099FF" android:pathData="M3,7C1.343,7 0,5.657 0,4C0,2.343 1.343,1 3,1C4.657,1 6,2.343 6,4C6,5.657 4.657,7 3,7Z" android:strokeAlpha="0.25"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M9.154,4.949C11.859,7.642 15.264,9.422 18.998,10.097C18.548,6.106 15.147,3 11.014,3C9.922,3 8.882,3.218 7.932,3.61C8.318,4.074 8.729,4.525 9.154,4.949ZM3,11.597C3.121,13.214 3.722,14.698 4.665,15.904L4.709,15.861C5.678,14.906 6.94,14.037 8.462,13.28C9.797,12.616 11.329,12.046 12.98,11.597H3ZM12.63,12.942L12.632,12.941L12.634,12.939L12.63,12.942ZM12.63,12.942C11.053,13.828 9.588,14.931 8.308,16.205C7.831,16.68 7.373,17.184 6.945,17.706L6.839,17.835C6.345,17.536 5.889,17.184 5.472,16.79L5.542,16.722C7.107,15.172 9.607,13.849 12.63,12.942ZM5.542,5.278C7.107,6.83 9.609,8.152 12.632,9.061C11.054,8.173 9.588,7.07 8.307,5.795C7.831,5.322 7.373,4.818 6.943,4.294L6.837,4.165C6.344,4.464 5.887,4.816 5.472,5.21L5.542,5.278ZM9.154,17.051C8.728,17.475 8.316,17.926 7.932,18.39C8.88,18.782 9.922,19 11.014,19C15.147,19 18.548,15.894 19,11.901C15.266,12.576 11.861,14.357 9.155,17.05L9.154,17.051ZM8.462,8.72C6.94,7.963 5.678,7.094 4.709,6.139L4.665,6.096C3.722,7.302 3.121,8.786 3,10.403H12.978C11.327,9.954 9.797,9.384 8.461,8.718L8.462,8.72Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="315dp"
android:height="198dp"
android:viewportWidth="315"
android:viewportHeight="198">
<path
android:pathData="M10,0L305,0A10,10 0,0 1,315 10L315,188A10,10 0,0 1,305 198L10,198A10,10 0,0 1,0 188L0,10A10,10 0,0 1,10 0z"
android:fillColor="#C9C9C9"/>
<path
android:pathData="M167.85,73.95H147.56C144.35,73.95 142.75,73.95 141.53,74.57C140.45,75.13 139.58,76.01 139.03,77.09C138.41,78.33 138.41,79.94 138.41,83.17V86.66H177V83.17C177,79.94 177,78.33 176.38,77.09C175.83,76.01 174.95,75.13 173.88,74.57C172.66,73.95 171.05,73.95 167.85,73.95Z"
android:fillColor="#4A4A4A"/>
<path
android:pathData="M164.13,97.13H177V113.35C177,116.57 177,118.18 176.37,119.42C175.83,120.5 174.95,121.38 173.88,121.94C172.65,122.56 171.05,122.56 167.85,122.56H164.13V97.13Z"
android:fillColor="#4A4A4A"/>
<path
android:pathData="M151.27,97.13H138.41V113.35C138.41,116.57 138.41,118.18 139.03,119.42C139.58,120.5 140.45,121.38 141.53,121.94C142.75,122.56 144.35,122.56 147.55,122.56H151.27V97.13Z"
android:fillColor="#4A4A4A"/>
</vector>

View file

@ -0,0 +1,7 @@
<vector 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:fillColor="#1E1E1E" android:fillType="evenOdd" android:pathData="M5,6.25C3.481,6.25 2.25,7.481 2.25,9V18C2.25,19.519 3.481,20.75 5,20.75H19C20.519,20.75 21.75,19.519 21.75,18V9C21.75,7.481 20.519,6.25 19,6.25H5ZM16.5,12.25C15.81,12.25 15.25,12.81 15.25,13.5C15.25,14.19 15.81,14.75 16.5,14.75C17.19,14.75 17.75,14.19 17.75,13.5C17.75,12.81 17.19,12.25 16.5,12.25Z"/>
<path android:fillColor="#1E1E1E" android:pathData="M16.485,3.069C17.754,2.73 19,3.687 19,5.001H9L16.485,3.069Z"/>
</vector>

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 786 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#ffffff"/>
<path
android:pathData="M0,0h22v22h-22z"
android:fillColor="#080A0B"/>
<path
android:pathData="M9.154,4.949C11.859,7.642 15.264,9.422 18.999,10.097C18.548,6.106 15.147,3 11.014,3C9.922,3 8.882,3.218 7.932,3.61C8.318,4.074 8.729,4.525 9.154,4.949ZM3,11.597C3.121,13.214 3.722,14.698 4.665,15.904L4.709,15.861C5.678,14.906 6.94,14.037 8.462,13.28C9.797,12.616 11.329,12.046 12.98,11.597H3ZM12.63,12.942L12.632,12.941L12.634,12.939L12.63,12.942ZM12.63,12.942C11.053,13.828 9.588,14.931 8.308,16.205C7.831,16.68 7.373,17.184 6.945,17.706L6.839,17.835C6.345,17.536 5.889,17.184 5.472,16.79L5.542,16.722C7.107,15.172 9.607,13.849 12.63,12.942ZM5.542,5.278C7.107,6.83 9.609,8.152 12.632,9.061C11.054,8.173 9.588,7.07 8.307,5.795C7.831,5.322 7.373,4.818 6.943,4.294L6.837,4.165C6.344,4.464 5.887,4.816 5.472,5.21L5.542,5.278ZM9.154,17.051C8.728,17.475 8.316,17.926 7.932,18.39C8.88,18.782 9.922,19 11.014,19C15.147,19 18.548,15.894 19,11.901C15.266,12.576 11.861,14.357 9.155,17.05L9.154,17.051ZM8.462,8.72C6.94,7.963 5.678,7.094 4.709,6.139L4.665,6.096C3.722,7.302 3.121,8.786 3,10.403H12.978C11.327,9.954 9.797,9.384 8.461,8.718L8.462,8.72Z"
android:fillColor="#F5F5F5"
android:fillType="evenOdd"/>
</group>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB