Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-21 12:09:33 +03:00
commit 85423227c4
390 changed files with 11140 additions and 1722 deletions

View file

@ -219,16 +219,24 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class Swap(
val currency: CryptoCurrency,
val currencyFrom: CryptoCurrency,
val currencyTo: CryptoCurrency? = null,
val userWalletId: UserWalletId,
val isInitialReverseOrder: Boolean = false,
) : AppRoute(path = "/swap/${currency.id.value}/${userWalletId.stringValue}/$isInitialReverseOrder"),
) : AppRoute(
path = "/swap" +
"/${currencyFrom.id.value}" +
"/${currencyTo?.id?.value}" +
"/${userWalletId.stringValue}" +
"/$isInitialReverseOrder",
),
RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val CURRENCY_BUNDLE_KEY = "currency"
const val CURRENCY_FROM_KEY = "currencyFrom"
const val CURRENCY_TO_KEY = "currencyTo"
const val USER_WALLET_ID_KEY = "userWalletId"
const val IS_INITIAL_REVERSE_ORDER = "isInitialReverseOrder"
}
@ -287,7 +295,16 @@ sealed class AppRoute(val path: String) : Route {
}
@Serializable
data object Onramp : AppRoute(path = "/onramp")
data class Onramp(val currency: CryptoCurrency) : AppRoute(path = "/onramp/${currency.symbol}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
@Serializable
data class OnrampSuccess(
val txId: String,
) : AppRoute(path = "/onramp/success/$txId"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
@Serializable
data class BuyCrypto(
@ -299,6 +316,14 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/sell_crypto/${userWalletId.stringValue}")
@Serializable
data class SwapCrypto(
val userWalletId: UserWalletId,
) : AppRoute(path = "/swap_crypto/${userWalletId.stringValue}")
// Onboarding V2
data class Onboarding(val scanResponse: ScanResponse) : AppRoute(path = "/onboarding_v2")
data class Onboarding(
val scanResponse: ScanResponse,
val startFromBackup: Boolean,
) : AppRoute(path = "/onboarding_v2${if (startFromBackup) "/backup" else ""}")
}

View file

@ -31,10 +31,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
@ -118,7 +115,7 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh
@Composable
private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest) {
FooterContainer(
footer = stringResource(id = R.string.give_permission_policy_type_footer),
footer = resourceReference(R.string.give_permission_policy_type_footer),
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
AmountItem(
@ -130,7 +127,7 @@ private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest)
}
SpacerH16()
FooterContainer(
footer = data.footerText.resolveReference(),
footer = data.footerText,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
FeeItem(fee = data.fee)

View file

@ -0,0 +1,228 @@
package com.tangem.common.ui.expressStatus
import androidx.annotation.DrawableRes
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
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.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.R
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.core.ui.components.SpacerWMax
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.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.persistentListOf
/**
* Block with express statuses
*
* @param state ui holder
* @param modifier modifier
* @see [Figma](https://www.figma.com/design/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?node-id=18459-26521&t=4jox7bfqUiXnm2h1-4)
*/
@Composable
fun ExpressStatusBlock(state: ExpressStatusUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.padding(
vertical = TangemTheme.dimens.spacing14,
horizontal = TangemTheme.dimens.spacing12,
),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing16),
) {
Text(
text = state.title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
SpacerWMax()
AnimatedVisibility(visible = state.link is ExpressLinkUM.Content) {
val link = remember(this) { state.link as ExpressLinkUM.Content }
Row(
modifier = Modifier.clickable { link.onClick() },
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = link.icon),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.spacing16)
.padding(end = TangemTheme.dimens.spacing2),
)
Text(
text = link.text.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
Column {
state.statuses.forEachIndexed { index, item ->
ExpressStatusStep(item, index == state.statuses.lastIndex)
}
}
}
}
@Composable
private fun ExpressStatusStep(status: ExpressStatusItemUM, isLast: Boolean) {
AnimatedContent(
targetState = status,
label = "Exchange Step Change Success",
transitionSpec = {
fadeIn(tween(durationMillis = 220)) togetherWith
fadeOut(tween(durationMillis = 220))
},
) { content ->
Row {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
when (content.state) {
ExpressStatusItemState.Active -> StepInProgress()
ExpressStatusItemState.Default -> StepDefault()
ExpressStatusItemState.Done -> Step(
iconRes = R.drawable.ic_check_24,
iconColor = TangemTheme.colors.icon.primary1,
borderColor = TangemTheme.colors.field.focused,
)
ExpressStatusItemState.Error -> Step(
iconRes = R.drawable.ic_close_24,
iconColor = TangemTheme.colors.icon.warning,
)
ExpressStatusItemState.Warning -> Step(
iconRes = R.drawable.ic_close_24,
iconColor = TangemTheme.colors.icon.attention,
)
}
if (!isLast) {
StepSeparator()
}
}
val textColor = when (status.state) {
ExpressStatusItemState.Active -> TangemTheme.colors.text.primary1
ExpressStatusItemState.Default -> TangemTheme.colors.text.disabled
ExpressStatusItemState.Done -> TangemTheme.colors.text.primary1
ExpressStatusItemState.Error -> TangemTheme.colors.text.warning
ExpressStatusItemState.Warning -> TangemTheme.colors.text.attention
}
Text(
text = content.text.resolveReference(),
style = TangemTheme.typography.body2,
color = textColor,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12),
)
}
}
}
@Composable
private fun StepDefault() {
Box(
modifier = Modifier
.size(TangemTheme.dimens.size20)
.border(
width = TangemTheme.dimens.size1_5,
color = TangemTheme.colors.field.focused,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun Step(iconColor: Color, @DrawableRes iconRes: Int, borderColor: Color = iconColor) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconColor,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.border(
width = TangemTheme.dimens.size1_5,
color = borderColor,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun StepInProgress() {
CircularProgressIndicator(
color = TangemTheme.colors.icon.primary1,
strokeWidth = TangemTheme.dimens.size2,
modifier = Modifier
.padding(TangemTheme.dimens.spacing2)
.size(TangemTheme.dimens.size14),
)
}
@Composable
private fun StepSeparator() {
Box(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing2)
.size(
width = TangemTheme.dimens.size1_5,
height = TangemTheme.dimens.size10,
)
.background(
color = TangemTheme.colors.field.focused,
shape = CircleShape,
),
)
}
@Preview
@Composable
private fun Preview_ExchangeStatusBlock() {
val state = ExpressStatusUM(
title = resourceReference(R.string.express_exchange_status_title),
link = ExpressLinkUM.Content(
icon = R.drawable.ic_alert_24,
text = resourceReference(R.string.common_go_to_provider),
onClick = {},
),
statuses = persistentListOf(
ExpressStatusItemUM(text = stringReference("Done"), state = ExpressStatusItemState.Done),
ExpressStatusItemUM(text = stringReference("Active"), state = ExpressStatusItemState.Active),
ExpressStatusItemUM(text = stringReference("Warning"), state = ExpressStatusItemState.Warning),
ExpressStatusItemUM(text = stringReference("Error"), state = ExpressStatusItemState.Error),
ExpressStatusItemUM(text = stringReference("Default"), state = ExpressStatusItemState.Default),
),
)
TangemThemePreview {
ExpressStatusBlock(state = state)
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.common.ui.expressStatus
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.ExpressNotificationsUM
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.TangemTheme
@Composable
fun ExpressStatusNotificationBlock(state: NotificationUM?) {
AnimatedContent(
targetState = state,
modifier = Modifier.padding(top = 12.dp),
label = "Express Status Notification Change",
) { notification ->
if (notification?.config != null) {
Notification(
config = notification.config,
iconTint = when (state) {
is ExpressNotificationsUM.NeedVerification -> TangemTheme.colors.icon.attention
is ExpressNotificationsUM.FailedByProvider -> TangemTheme.colors.icon.warning
else -> null
},
containerColor = TangemTheme.colors.background.action,
)
}
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.common.ui.expressStatus.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
/**
* UI data holder for express status block
*
* @property title block title
* @property link provider web link
* @property statuses list of possible and active statuses
*/
data class ExpressStatusUM(
val title: TextReference,
val link: ExpressLinkUM,
val statuses: ImmutableList<ExpressStatusItemUM>,
)
/**
* Provider web link for express status block.
* [Empty] if no link needed
* [Content] if link is provided and displayed
*/
@Stable
sealed class ExpressLinkUM {
data object Empty : ExpressLinkUM()
data class Content(
@DrawableRes val icon: Int,
val text: TextReference,
val onClick: () -> Unit,
) : ExpressLinkUM()
}
/**
* Single status item in express status block
*/
data class ExpressStatusItemUM(
val text: TextReference,
val state: ExpressStatusItemState,
)
/**
* Available status states for express status block
*/
enum class ExpressStatusItemState {
Active,
Default,
Done,
Warning,
Error,
;
}

View file

@ -0,0 +1,28 @@
package com.tangem.common.ui.notifications
import com.tangem.common.ui.R
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resourceReference
object ExpressNotificationsUM {
data class NeedVerification(val onGoToProviderClick: () -> Unit) : NotificationUM.Warning(
title = resourceReference(R.string.express_exchange_notification_verification_title),
subtitle = resourceReference(R.string.express_exchange_notification_verification_text),
iconResId = R.drawable.ic_alert_triangle_20,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_go_to_provider),
onClick = onGoToProviderClick,
),
)
data class FailedByProvider(val onGoToProviderClick: () -> Unit) : NotificationUM.Error(
title = resourceReference(R.string.express_exchange_notification_failed_title),
subtitle = resourceReference(R.string.express_exchange_notification_failed_text),
iconResId = R.drawable.ic_alert_circle_24,
buttonState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_go_to_provider),
onClick = onGoToProviderClick,
),
)
}

View file

@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.shorted
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import java.math.BigDecimal
sealed class NotificationUM(val config: NotificationConfig) {
@ -307,4 +308,17 @@ sealed class NotificationUM(val config: NotificationConfig) {
),
)
}
sealed interface Solana {
data class RentInfo(
private val rentInfo: CryptoCurrencyWarning.Rent,
) : Info(
title = TextReference.Res(R.string.warning_rent_fee_title),
subtitle = TextReference.Res(
id = R.string.warning_solana_rent_fee_message,
formatArgs = wrappedList(rentInfo.rent, rentInfo.exemptionAmount),
),
)
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIco
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
@ -39,8 +40,8 @@ class TokenItemStateConverter(
private val fiatAmountStateProvider: (CryptoCurrencyStatus) -> TokenItemState.FiatAmountState? = {
createFiatAmountState(it, appCurrency)
},
private val onItemClick: (CryptoCurrencyStatus) -> Unit,
private val onItemLongClick: ((CryptoCurrencyStatus) -> Unit)? = null,
private val onItemClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null,
private val onItemLongClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
@ -75,9 +76,11 @@ class TokenItemStateConverter(
subtitleState = requireNotNull(subtitleStateProvider(this)),
fiatAmountState = requireNotNull(fiatAmountStateProvider(this)),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = getFormattedAmount()),
onItemClick = { onItemClick(this) },
onItemLongClick = onItemLongClick?.let {
{ it(this) }
onItemClick = onItemClick?.let { onItemClick ->
{ onItemClick(it, this) }
},
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
{ onItemLongClick(it, this) }
},
)
}
@ -94,9 +97,11 @@ class TokenItemStateConverter(
iconState = iconStateProvider(this),
titleState = titleStateProvider(this),
subtitleState = subtitleStateProvider(this),
onItemClick = { onItemClick(this) },
onItemLongClick = onItemLongClick?.let {
{ it(this) }
onItemClick = onItemClick?.let { onItemClick ->
{ onItemClick(it, this) }
},
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
{ onItemLongClick(it, this) }
},
)
}
@ -107,8 +112,8 @@ class TokenItemStateConverter(
iconState = iconStateProvider(this),
titleState = titleStateProvider(this),
subtitleState = subtitleStateProvider(this),
onItemLongClick = onItemLongClick?.let {
{ it(this) }
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
{ onItemLongClick(it, this) }
},
)
}
@ -122,7 +127,7 @@ class TokenItemStateConverter(
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> {
TokenItemState.TitleState.Content(text = currencyStatus.currency.name)
TokenItemState.TitleState.Content(text = stringReference(currencyStatus.currency.name))
}
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
@ -130,7 +135,7 @@ class TokenItemStateConverter(
is CryptoCurrencyStatus.NoAccount,
-> {
TokenItemState.TitleState.Content(
text = currencyStatus.currency.name,
text = stringReference(currencyStatus.currency.name),
hasPending = value.hasCurrentNetworkTransactions,
)
}