Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-24 18:28:09 +03:00
parent 464f8f8a80
commit d10cf0ac2e
23 changed files with 1117 additions and 10 deletions

View file

@ -21,7 +21,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/onboarding_balance_title"
android:text="@string/common_balance_title"
android:textColor="@color/text_tertiary"
android:textSize="14sp"
android:textStyle="bold"

View file

@ -12,7 +12,7 @@
android:elevation="0dp"
android:fontFamily="sans-serif-medium"
android:letterSpacing="0.036"
android:text="@string/onboarding_balance_title"
android:text="@string/common_balance_title"
android:textAllCaps="true"
android:textColor="@color/text_secondary"
android:textSize="14sp"

View file

@ -192,6 +192,11 @@
<string name="exchange_tokens_unavailable_tokens_header">Недоступен для обмена с %s</string>
<string name="express_choose_providers_subtitle">Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами</string>
<string name="express_choose_providers_title">Выберите провайдера</string>
<string name="express_exchange_notification_failed_text">Чтобы узнать причину, посетите сайт провайдера</string>
<string name="express_exchange_notification_failed_title">Операция не выполнена провайдером</string>
<string name="express_exchange_notification_verification_text">Посетите сайт провайдера для проверки</string>
<string name="express_exchange_notification_verification_title">Провайдер: требуется верификация</string>
<string name="express_provider">Провайдер</string>
<string name="express_provider_best_rate">Лучший курс</string>
<string name="express_provider_permission_needed">Требуется разрешение</string>
<string name="feedback_data_collection_message">Информация ниже не является обязательной. Вы можете стереть её, если хотите.</string>
@ -266,7 +271,7 @@
<string name="onboarding_access_codes_doesnt_match">Введенные коды доступа не совпадают</string>
<string name="onboarding_alert_message_not_max_backup_cards_added">Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить?</string>
<string name="onboarding_backup_exit_warning">Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас.</string>
<string name="onboarding_balance_title">Баланс</string>
<string name="common_balance_title">Баланс</string>
<string name="onboarding_button_add_backup_card">Добавить резервную карту</string>
<string name="onboarding_button_backup_card_format">Сканировать карту #%d</string>
<string name="onboarding_button_backup_now">Создать резервную копию</string>

View file

@ -178,7 +178,7 @@
<string name="onboarding_access_codes_doesnt_match">輸入的訪問密碼與初始訪問密碼不匹配</string>
<string name="onboarding_alert_message_not_max_backup_cards_added">您已添加一張備用卡。備份過程完成後,您將無法添加更多備份卡。如果您還有一張卡,請將其添加到備份中。您想繼續備份過程嗎?</string>
<string name="onboarding_backup_exit_warning">備份過程已部分完成。你現在不能退出</string>
<string name="onboarding_balance_title">餘額</string>
<string name="common_balance_title">餘額</string>
<string name="onboarding_button_add_backup_card">添加備用卡</string>
<string name="onboarding_button_backup_card_format">掃描卡片 #%d</string>
<string name="onboarding_button_backup_now">立即備份</string>

View file

@ -192,7 +192,30 @@
<string name="exchange_tokens_unavailable_tokens_header">Unavailable for swap from %s</string>
<string name="express_choose_providers_subtitle">Providers facilitate transactions, ensuring smooth and efficient token exchanges</string>
<string name="express_choose_providers_title">Choose Provider</string>
<string name="express_provider_best_rate">Best Rate</string>
<string name="express_estimated_amount">Estimated amount</string>
<string name="express_exchange_notification_failed_text">Visit providers website to see why</string>
<string name="express_exchange_notification_failed_title">Operation failed by provider</string>
<string name="express_exchange_notification_verification_text">Visit providers website for verification</string>
<string name="express_exchange_notification_verification_title">Provider: Verification needed</string>
<string name="express_exchange_status_confirmed">Confirmed</string>
<string name="express_exchange_status_confirming">Confirming</string>
<string name="express_exchange_status_exchanged">Exchanged</string>
<string name="express_exchange_status_exchanging">Exchanging</string>
<string name="express_exchange_status_failed">Failed</string>
<string name="express_exchange_status_received">Deposit received</string>
<string name="express_exchange_status_receiving">Awaiting deposit</string>
<string name="express_exchange_status_sending">Sending to you</string>
<string name="express_exchange_status_sent">Sent</string>
<string name="express_exchange_status_subtitle">Provider-sourced data. Estimated amount subject to change.</string>
<string name="express_exchange_status_title">Exchange status</string>
<string name="express_exchange_status_verified">Verified</string>
<string name="express_exchange_status_verifying">Verification required</string>
<string name="express_fetch_best_rates">Fetching best rates...</string>
<string name="express_floating_rate">Floating rate</string>
<string name="express_go_to_provider">Go to provider</string>
<string name="express_provider">Provider</string>
<string name="express_provider_best_rate">Best rate</string>
<string name="express_exchange_by">Exchange by %s</string>
<string name="express_provider_permission_needed">Permission Needed</string>
<string name="feedback_data_collection_message">The following information is optional. You can erase it if you don\'t want to share it.</string>
<string name="feedback_preface_rate_negative">Tell us what functions you are missing, and we will try to help you.</string>
@ -267,7 +290,7 @@
<string name="onboarding_access_codes_doesnt_match">Entered access code didn\'t match the initial access code</string>
<string name="onboarding_alert_message_not_max_backup_cards_added">You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process?</string>
<string name="onboarding_backup_exit_warning">The backup process is partly complete. You can\'t exit it now.</string>
<string name="onboarding_balance_title">Balance</string>
<string name="common_balance_title">Balance</string>
<string name="onboarding_button_add_backup_card">Add a backup card</string>
<string name="onboarding_button_backup_card_format">Scan the card #%d</string>
<string name="onboarding_button_backup_now">Backup now</string>

View file

@ -6,6 +6,7 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.res.TangemTheme
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@ -20,6 +21,7 @@ import kotlinx.coroutines.launch
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
config: TangemBottomSheetConfig,
color: Color = TangemTheme.colors.background.primary,
crossinline content: @Composable ColumnScope.(T) -> Unit,
) {
var isVisible by remember { mutableStateOf(value = config.isShow) }
@ -29,9 +31,9 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
ModalBottomSheet(
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = TangemTheme.colors.background.primary,
containerColor = color,
shape = TangemTheme.shapes.bottomSheetLarge,
dragHandle = { TangemBottomSheetDraggableHeader() },
dragHandle = { TangemBottomSheetDraggableHeader(color) },
) {
content(config.content)
}

View file

@ -0,0 +1,150 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* [Input Row Approx](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2207-810&mode=design&t=fM1ZU6zQF6g3CaTv-4)
*
* @param leftIcon left token state
* @param leftTitle left token title
* @param leftSubtitle left token subtitle
* @param rightIcon right token state
* @param rightTitle right token title
* @param rightSubtitle right token subtitle
* @param modifier composable modifier
* @param showDivider show divider
*/
@Suppress("LongParameterList")
@Composable
fun InputRowApprox(
leftIcon: TokenIconState,
leftTitle: TextReference,
leftSubtitle: TextReference,
rightIcon: TokenIconState,
rightTitle: TextReference,
rightSubtitle: TextReference,
modifier: Modifier = Modifier,
showDivider: Boolean = false,
) {
DividerContainer(
showDivider = showDivider,
modifier = modifier,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(TangemTheme.dimens.spacing12)
.fillMaxWidth(),
) {
InputRowApproxItem(
iconState = leftIcon,
title = leftTitle,
subtitle = leftSubtitle,
)
Icon(
painter = painterResource(id = R.drawable.ic_approx_24),
contentDescription = null,
tint = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(
horizontal = TangemTheme.dimens.spacing8,
vertical = TangemTheme.dimens.spacing10,
),
)
InputRowApproxItem(
iconState = rightIcon,
title = rightTitle,
subtitle = rightSubtitle,
)
}
}
}
@Composable
private fun InputRowApproxItem(
iconState: TokenIconState,
title: TextReference,
subtitle: TextReference,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
) {
TokenIcon(
state = iconState,
modifier = Modifier
.size(TangemTheme.dimens.size36),
)
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing12,
),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing2),
)
}
}
}
//region Preview
@Preview
@Composable
private fun InputRowApproxPreview_Light() {
TangemTheme {
InputRowApprox(
leftIcon = TokenIconState.Loading,
leftTitle = TextReference.Str("Left title"),
leftSubtitle = TextReference.Str("Left subtitle"),
rightIcon = TokenIconState.Loading,
rightTitle = TextReference.Str("Right title"),
rightSubtitle = TextReference.Str("Right subtitle"),
modifier = Modifier
.background(TangemTheme.colors.background.action),
)
}
}
@Preview
@Composable
private fun InputRowApproxPreview_Dark() {
TangemTheme(isDark = true) {
InputRowApprox(
leftIcon = TokenIconState.Loading,
leftTitle = TextReference.Str("Left title"),
leftSubtitle = TextReference.Str("Left subtitle"),
rightIcon = TokenIconState.Loading,
rightTitle = TextReference.Str("Right title"),
rightSubtitle = TextReference.Str("Right subtitle"),
modifier = Modifier
.background(TangemTheme.colors.background.action),
)
}
}
//endregion

View file

@ -0,0 +1,221 @@
package com.tangem.core.ui.components.inputrow
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.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* [Input Row Best Rate](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-889&mode=dev)
*
* @param imageUrl image source url
* @param title title
* @param titleExtra title extra
* @param subtitle subtitle
* @param modifier composable modifier
* @param showTag show tag
* @param showDivider show divider
* @param onIconClick icon click
*/
@Composable
fun InputRowBestRate(
imageUrl: String,
title: TextReference,
titleExtra: TextReference,
subtitle: TextReference,
modifier: Modifier = Modifier,
showTag: Boolean = false,
showDivider: Boolean = false,
onIconClick: (() -> Unit)? = null,
) {
DividerContainer(
showDivider = showDivider,
modifier = modifier,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(TangemTheme.dimens.spacing12),
) {
InnerIcon(imageUrl = imageUrl)
Column(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12),
) {
InnerTitle(
title = title,
titleExtra = titleExtra,
showTag = showTag,
)
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
SpacerWMax()
onIconClick?.let {
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing10)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
onClick = onIconClick,
),
)
}
}
}
}
@Composable
private fun InnerTitle(title: TextReference, titleExtra: TextReference, showTag: Boolean = false) {
Row {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = titleExtra.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing4),
)
if (showTag) {
Text(
text = stringResource(R.string.express_provider_best_rate),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.icon.accent,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing4)
.background(
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
shape = RoundedCornerShape(TangemTheme.dimens.radius20),
)
.padding(horizontal = TangemTheme.dimens.spacing6),
)
}
}
}
@Composable
private fun InnerIcon(imageUrl: String) {
SubcomposeAsyncImage(
modifier = Modifier.size(TangemTheme.dimens.size40),
model = ImageRequest.Builder(context = LocalContext.current)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = { LoadingIcon() },
error = {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.tertiary,
shape = CircleShape,
),
)
},
contentDescription = null,
)
}
//region preview
@Preview
@Composable
private fun InputRowBestRatePreview_Light(
@PreviewParameter(InputRowBestRatePreviewDataProvider::class) data: InputRowBestRatePreviewData,
) {
TangemTheme {
InputRowBestRate(
imageUrl = "",
title = data.title,
titleExtra = data.titleExtra,
subtitle = data.subtitle,
showTag = data.showTag,
onIconClick = data.iconClick,
modifier = Modifier
.background(TangemTheme.colors.background.action),
)
}
}
@Preview
@Composable
private fun InputRowBestRatePreview_Dark(
@PreviewParameter(InputRowBestRatePreviewDataProvider::class) data: InputRowBestRatePreviewData,
) {
TangemTheme(isDark = true) {
InputRowBestRate(
imageUrl = "",
title = data.title,
titleExtra = data.titleExtra,
subtitle = data.subtitle,
showTag = data.showTag,
onIconClick = data.iconClick,
modifier = Modifier
.background(TangemTheme.colors.background.action),
)
}
}
private data class InputRowBestRatePreviewData(
val title: TextReference,
val titleExtra: TextReference,
val showTag: Boolean,
val subtitle: TextReference,
val iconClick: (() -> Unit)?,
)
private class InputRowBestRatePreviewDataProvider : PreviewParameterProvider<InputRowBestRatePreviewData> {
override val values: Sequence<InputRowBestRatePreviewData>
get() = sequenceOf(
InputRowBestRatePreviewData(
title = TextReference.Str("1inch"),
titleExtra = TextReference.Str("DEX"),
subtitle = TextReference.Str("0,64554846 DAI ≈ 1 MATIC "),
showTag = true,
iconClick = {},
),
InputRowBestRatePreviewData(
title = TextReference.Str("ChangeNow"),
titleExtra = TextReference.Str("CEX"),
subtitle = TextReference.Str("0,64554846 DAI ≈ 1 MATIC "),
showTag = false,
iconClick = null,
),
)
}
//endregion

View file

@ -41,6 +41,7 @@ data class TangemDimens internal constructor(
val size0: Dp = 0.dp,
val size0_5: Dp = 0.5.dp,
val size1: Dp = 1.dp,
val size1_5: Dp = 1.5.dp,
val size2: Dp = 2.dp,
val size4: Dp = 4.dp,
val size5: Dp = 5.dp,

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M9.135,2.328L1.702,15.165C1.316,15.832 1.797,16.667 2.568,16.667H17.432C18.202,16.667 18.684,15.832 18.298,15.165L10.865,2.328C10.48,1.663 9.52,1.663 9.135,2.328ZM10.833,11.667H9.167V6.667H10.833V11.667ZM10.833,15H9.167V13.333H10.833V15Z"
android:fillColor="#FFB71B"
android:fillType="evenOdd"/>
</vector>

View 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="M14.153,11.13C12.544,11.13 11.142,9.695 9.731,9.695C8.625,9.695 7.825,10.153 7.25,10.798V9.076C7.816,8.457 8.652,8 9.847,8C11.456,8 12.867,9.435 14.269,9.435C15.366,9.435 16.175,8.978 16.75,8.332V10.054C16.184,10.673 15.348,11.13 14.153,11.13ZM14.153,16C12.544,16 11.142,14.565 9.731,14.565C8.625,14.565 7.825,15.022 7.25,15.668V13.946C7.816,13.327 8.652,12.87 9.847,12.87C11.456,12.87 12.867,14.305 14.269,14.305C15.366,14.305 16.175,13.856 16.75,13.202V14.924C16.184,15.543 15.348,16 14.153,16Z"
android:fillColor="#909090"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11,13H13V7H11V13Z"
android:fillColor="#FFB71B"/>
<path
android:pathData="M11,17H13V15H11V17Z"
android:fillColor="#FFB71B"/>
</vector>

View 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="M4.16,11V13H16.16L10.66,18.5L12.08,19.92L20,12L12.08,4.08L10.66,5.5L16.16,11H4.16Z"
android:fillColor="#1E1E1E"/>
</vector>

View file

@ -0,0 +1,20 @@
package com.tangem.feature.swap.domain.models.domain
data class ExchangeStatusModel(
val providerId: Int,
val status: ExchangeStatus? = null,
val txId: String? = null,
val txUrl: String? = null,
)
enum class ExchangeStatus {
New,
Waiting,
Confirming,
Verifying,
Exchanging,
Failed,
Sending,
Finished,
Refunded,
}

View file

@ -28,6 +28,7 @@ dependencies {
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.ui.utils)
implementation(deps.compose.constraintLayout)
implementation(deps.arrow.core)
implementation(deps.jodatime)
@ -68,4 +69,5 @@ dependencies {
/** Feature Apis */
implementation(projects.features.tokendetails.api)
implementation(projects.features.send.api)
implementation(projects.features.swap.domain)
}

View file

@ -0,0 +1,31 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import kotlinx.collections.immutable.PersistentList
import java.math.BigDecimal
internal data class SwapTransactionsState(
val txId: String,
val providerId: Int,
val txUrl: String? = null,
val rate: BigDecimal,
val timestamp: Long,
val status: PersistentList<ExchangeStatusState>,
val activeStatus: ExchangeStatus?,
val toCryptoAmount: String,
val toFiatAmount: String,
val toCurrencyIcon: TokenIconState,
val fromCryptoAmount: String,
val fromFiatAmount: String,
val fromCurrencyIcon: TokenIconState,
val onClick: () -> Unit,
val onGoToProviderClick: () -> Unit,
)
internal class ExchangeStatusState(
val status: ExchangeStatus,
val text: String,
val isActive: Boolean,
val isDone: Boolean,
)

View file

@ -39,7 +39,7 @@ internal fun TokenDetailsBalanceBlock(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
),
text = stringResource(id = R.string.onboarding_balance_title),
text = stringResource(id = R.string.common_balance_title),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
maxLines = 1,

View file

@ -0,0 +1,67 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
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.res.stringResource
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.components.inputrow.InputRowApprox
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.tokendetails.impl.R
@Suppress("LongParameterList")
@Composable
internal fun ExchangeEstimate(
timestamp: TextReference,
fromTokenIconState: TokenIconState,
toTokenIconState: TokenIconState,
fromCryptoAmount: TextReference,
toCryptoAmount: TextReference,
fromFiatAmount: TextReference,
toFiatAmount: TextReference,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing14,
bottom = TangemTheme.dimens.spacing2,
),
) {
Text(
text = stringResource(id = R.string.express_estimated_amount),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
Text(
text = timestamp.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
InputRowApprox(
leftIcon = fromTokenIconState,
leftTitle = fromCryptoAmount,
leftSubtitle = fromFiatAmount,
rightIcon = toTokenIconState,
rightTitle = toCryptoAmount,
rightSubtitle = toFiatAmount,
)
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.R
import com.tangem.core.ui.components.inputrow.InputRowBestRate
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun ExchangeProvider(providerName: TextReference, providerType: TextReference, imageUrl: String) {
Column(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
) {
Text(
text = stringResource(id = R.string.express_provider),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing12,
),
)
InputRowBestRate(
imageUrl = imageUrl,
title = providerName,
titleExtra = providerType,
subtitle = TextReference.Res(R.string.express_floating_rate),
)
}
}

View file

@ -0,0 +1,209 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
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.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material3.Text
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 androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.PersistentList
@Composable
internal fun ExchangeStatusBlock(
status: PersistentList<ExchangeStatusState>,
onClick: () -> Unit,
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 = stringResource(id = R.string.common_balance_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
SpacerWMax()
Row(
modifier = Modifier.clickable { onClick() },
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = R.drawable.ic_arrow_top_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.spacing16)
.padding(end = TangemTheme.dimens.spacing2),
)
Text(
text = stringResource(id = R.string.express_go_to_provider),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
status.forEachIndexed { index, item ->
ExchangeStatusStep(
stepStatus = item,
isLast = index == status.lastIndex,
)
}
}
}
@Composable
private fun ExchangeStatusStep(
stepStatus: ExchangeStatusState,
modifier: Modifier = Modifier,
isLast: Boolean = false,
) {
Row(modifier = modifier) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
AnimatedContent(
targetState = stepStatus,
label = "Exchange Step Change Success",
modifier = Modifier
.size(TangemTheme.dimens.size20),
) {
when {
it.status == ExchangeStatus.Failed && !it.isDone -> ExchangeStepWaringOrError(
iconRes = R.drawable.ic_close_24,
color = TangemTheme.colors.icon.warning,
)
it.status == ExchangeStatus.Verifying && !it.isDone -> ExchangeStepWaringOrError(
iconRes = R.drawable.ic_exclamation_24,
color = TangemTheme.colors.icon.attention,
)
it.isDone -> ExchangeStepSuccess()
it.isActive -> ExchangeStepInProgress()
else -> ExchangeStepDefault()
}
}
if (!isLast) {
ExchangeStepSeparator()
}
}
ExchangeStatusStepText(stepStatus)
}
}
@Composable
private fun ExchangeStatusStepText(stepStatus: ExchangeStatusState) {
val textColor = when {
stepStatus.status == ExchangeStatus.Failed && !stepStatus.isDone -> TangemTheme.colors.icon.warning
stepStatus.status == ExchangeStatus.Verifying && !stepStatus.isDone -> TangemTheme.colors.icon.attention
stepStatus.isDone -> TangemTheme.colors.text.primary1
!stepStatus.isActive -> TangemTheme.colors.text.disabled
else -> TangemTheme.colors.text.primary1
}
Text(
text = stepStatus.text,
style = TangemTheme.typography.body2,
color = textColor,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12),
)
}
@Composable
private fun ExchangeStepDefault() {
Box(
modifier = Modifier
.border(
width = TangemTheme.dimens.size1_5,
color = TangemTheme.colors.field.focused,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun ExchangeStepSuccess() {
Icon(
painter = painterResource(id = R.drawable.ic_check_24),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
modifier = Modifier
.border(
width = TangemTheme.dimens.size1_5,
color = TangemTheme.colors.field.focused,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun ExchangeStepWaringOrError(color: Color, @DrawableRes iconRes: Int) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = color,
modifier = Modifier
.border(
width = TangemTheme.dimens.size1_5,
color = color,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun ExchangeStepInProgress() {
CircularProgressIndicator(
color = TangemTheme.colors.icon.primary1,
strokeWidth = TangemTheme.dimens.size2,
modifier = Modifier
.padding(TangemTheme.dimens.spacing2)
.size(TangemTheme.dimens.size14),
)
}
@Composable
private fun ExchangeStepSeparator() {
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,
),
)
}

View file

@ -0,0 +1,84 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.toDateFormat
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
@Composable
internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
color = TangemTheme.colors.background.tertiary,
) { content: ExchangeStatusBottomSheetConfig ->
ExchangeStatusBottomSheetContent(content = content)
}
}
@Composable
private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetConfig) {
val config = content.value
Column(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
SpacerH10()
Text(
text = stringResource(id = R.string.express_exchange_status_title),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.align(CenterHorizontally),
)
SpacerH10()
Text(
text = stringResource(id = R.string.express_exchange_status_subtitle),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier
.align(CenterHorizontally),
)
SpacerH16()
val timestamp = config.timestamp
ExchangeEstimate(
timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"),
fromTokenIconState = config.fromCurrencyIcon,
toTokenIconState = config.toCurrencyIcon,
fromCryptoAmount = TextReference.Str(config.fromCryptoAmount),
toCryptoAmount = TextReference.Str(config.toCryptoAmount),
fromFiatAmount = TextReference.Str(config.fromFiatAmount),
toFiatAmount = TextReference.Str(config.toFiatAmount),
)
SpacerH12()
// todo replace with real provider data
ExchangeProvider(
providerName = TextReference.Str(config.providerId.toString()),
providerType = TextReference.Str("CEX"),
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/changenow_512.png",
)
SpacerH12()
ExchangeStatusBlock(
status = config.status,
onClick = config.onGoToProviderClick,
)
SpacerH24()
}
}
internal data class ExchangeStatusBottomSheetConfig(
val value: SwapTransactionsState,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,180 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
import androidx.annotation.DrawableRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.res.stringResource
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Visibility
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.PersistentList
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.swapTransactionsItems(
swapTxs: PersistentList<SwapTransactionsState>,
modifier: Modifier = Modifier,
) {
if (swapTxs.isNotEmpty()) {
items(
count = swapTxs.size,
key = { swapTxs[it].txId },
contentType = { swapTxs[it]::class.java },
) {
val item = swapTxs[it]
val (iconRes, tint) = when (item.activeStatus) {
ExchangeStatus.Verifying -> R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention
ExchangeStatus.Failed -> R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning
else -> null to null
}
ExchangeStatusItem(
providerName = item.providerId.toString(),
fromTokenIconState = item.fromCurrencyIcon,
toTokenIconState = item.toCurrencyIcon,
fromAmount = item.fromCryptoAmount,
toAmount = item.toCryptoAmount,
onClick = item.onClick,
infoIconRes = iconRes,
infoIconTint = tint,
modifier = modifier.animateItemPlacement(),
)
}
}
}
@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod", "LongParameterList")
@Composable
private fun ExchangeStatusItem(
providerName: String,
fromTokenIconState: TokenIconState,
toTokenIconState: TokenIconState,
fromAmount: String,
toAmount: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
@DrawableRes infoIconRes: Int? = null,
infoIconTint: Color? = null,
) {
ConstraintLayout(
modifier = modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable { onClick() }
.padding(TangemTheme.dimens.spacing12),
) {
val (titleRef, iconRef, infoIconRef, swapIconRef, fromRef, toRef, fromIconRef, toIconRef) = createRefs()
val padding6 = TangemTheme.dimens.spacing6
Text(
text = stringResource(id = R.string.express_exchange_by, providerName),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.constrainAs(titleRef) {
start.linkTo(parent.start)
top.linkTo(parent.top)
},
)
TokenIcon(
state = fromTokenIconState,
shouldDisplayNetwork = false,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.constrainAs(fromIconRef) {
start.linkTo(parent.start)
top.linkTo(titleRef.bottom, padding6)
bottom.linkTo(parent.bottom)
},
)
Text(
text = fromAmount,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.constrainAs(fromRef) {
start.linkTo(fromIconRef.end, padding6)
top.linkTo(titleRef.bottom, padding6)
bottom.linkTo(parent.bottom)
},
)
Icon(
painter = painterResource(id = R.drawable.ic_forward_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.size12)
.constrainAs(swapIconRef) {
start.linkTo(fromRef.end, padding6)
top.linkTo(titleRef.bottom, padding6)
bottom.linkTo(parent.bottom)
},
)
TokenIcon(
state = toTokenIconState,
shouldDisplayNetwork = false,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.constrainAs(toIconRef) {
start.linkTo(swapIconRef.end, padding6)
top.linkTo(titleRef.bottom, padding6)
bottom.linkTo(parent.bottom)
},
)
Text(
text = toAmount,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.constrainAs(toRef) {
start.linkTo(toIconRef.end, padding6)
top.linkTo(titleRef.bottom, padding6)
bottom.linkTo(parent.bottom)
},
)
Icon(
painter = painterResource(id = infoIconRes ?: R.drawable.ic_alert_triangle_20),
contentDescription = null,
tint = infoIconTint ?: TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.constrainAs(infoIconRef) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
end.linkTo(iconRef.start)
visibility = if (infoIconRes == null) {
Visibility.Gone
} else {
Visibility.Visible
}
},
)
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.size24)
.constrainAs(iconRef) {
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
},
)
}
}

View file

@ -475,4 +475,35 @@ internal class TokenDetailsViewModel @Inject constructor(
override fun onCloseRentInfoNotification() {
uiState = stateFactory.getStateWithRemovedRentNotification()
}
}
}
//
// val text = when (stepStatus.status) {
// ExchangeStatus.Failed -> stringResource(id = R.string.express_exchange_status_failed)
// ExchangeStatus.Verifying -> if (stepStatus.isDone) {
// stringResource(id = R.string.express_exchange_status_verified)
// } else {
// stringResource(id = R.string.express_exchange_status_verifying)
// }
// ExchangeStatus.New, ExchangeStatus.Waiting -> if (stepStatus.isDone) {
// stringResource(id = R.string.express_exchange_status_received)
// } else {
// stringResource(id = R.string.express_exchange_status_receiving)
// }
// ExchangeStatus.Confirming -> if (stepStatus.isDone) {
// stringResource(id = R.string.express_exchange_status_confirmed)
// } else {
// stringResource(id = R.string.express_exchange_status_confirming)
// }
// ExchangeStatus.Exchanging -> if (stepStatus.isDone) {
// stringResource(id = R.string.express_exchange_status_exchanged)
// } else {
// stringResource(id = R.string.express_exchange_status_exchanging)
// }
// ExchangeStatus.Sending -> if (stepStatus.isDone) {
// stringResource(id = R.string.express_exchange_status_sent)
// } else {
// stringResource(id = R.string.express_exchange_status_sending)
// }
// else -> ""
// }