Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-01 20:28:08 +05:00
commit 5db12ad1e6
24 changed files with 722 additions and 250 deletions

View file

@ -4,7 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -13,14 +13,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
@ -30,11 +30,11 @@ import com.tangem.core.ui.res.TangemThemePreview
@Composable
fun AmountBlockV2(
amountState: AmountState,
currencyIconState: CurrencyIconState,
isClickDisabled: Boolean,
isEditingDisabled: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
extraContent: @Composable () -> Unit = {},
) {
if (amountState !is AmountState.Data) return
val amount = amountState.amountTextField
@ -48,7 +48,7 @@ fun AmountBlockV2(
val fiatAmount = amount.fiatAmount.value.format {
fiat(
fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
fiatCurrencySymbol = amountState.appCurrency.symbol,
fiatCurrencyCode = amountState.appCurrency.code,
)
}
@ -58,33 +58,26 @@ fun AmountBlockV2(
} else {
cryptoAmount to fiatAmount
}
val title = TextReference.Str(
stringResourceSafe(
R.string.send_from_wallet_name,
amountState.title
.resolveReference(),
),
)
val currencyTitle = amount.cryptoAmount.currencySymbol
AmountBlockV2(
title = title,
title = amountState.title,
balance = amountState.availableBalance,
currencyTitle = currencyTitle,
currencyIconState = currencyIconState,
currencyIconState = amountState.tokenIconState,
firstAmount = firstAmount,
secondAmount = secondAmount,
isClickDisabled = isClickDisabled,
isEditingDisabled = isEditingDisabled,
onClick = onClick,
modifier = modifier,
extraContent = extraContent,
)
}
@Suppress("LongParameterList")
@Composable
internal fun AmountBlockV2(
private fun AmountBlockV2(
title: TextReference,
balance: TextReference,
currencyTitle: String,
@ -93,14 +86,17 @@ internal fun AmountBlockV2(
secondAmount: String,
isClickDisabled: Boolean,
isEditingDisabled: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
extraContent: @Composable () -> Unit = {},
) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
.conditional(onClick != null) {
clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick!!)
}
.padding(TangemTheme.dimens.spacing16),
) {
Row {
@ -109,7 +105,7 @@ internal fun AmountBlockV2(
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
Spacer(modifier = modifier.weight(1f))
SpacerWMax()
Text(
text = balance.resolveReference(),
style = TangemTheme.typography.body2,
@ -130,11 +126,16 @@ internal fun AmountBlockV2(
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = secondAmount,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = secondAmount,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
extraContent()
}
}
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
@ -163,7 +164,6 @@ private fun AmountBlockPreview(@PreviewParameter(AmountBlockV2PreviewProvider::c
TangemThemePreview {
AmountBlockV2(
amountState = value,
currencyIconState = CurrencyIconState.Empty(),
isClickDisabled = false,
isEditingDisabled = false,
onClick = {},

View file

@ -574,13 +574,13 @@
<string name="nft_receive_unavailable_asset_warning_title">Netzwerk nicht hinzugefügt</string>
<string name="nft_receive_unavailable_section_title">Nicht hinzugefügt</string>
<string name="nft_receive_unsupported_types">Nicht unterstützte NFT-Arten</string>
<string name="nft_receive_unsupported_types_description">cNFTs und pNFTs werden noch nicht unterstützt</string>
<string name="nft_receive_unsupported_types_description">cNFTs und pNFTs werden derzeit nicht unterstützt. Bitte sende diese nicht an Deine Wallet.</string>
<string name="nft_send">NFT senden</string>
<string name="nft_traits_title">Eigenschaften</string>
<string name="nft_untitled_collection">Unbetitelte Sammlung</string>
<string name="nft_wallet_count">%1$d NFTs in der %2$d Sammlung</string>
<plurals name="nft_wallet_count_android">
<item quantity="one"></item>
<item quantity="one">%1$d NFT in der %2$d Sammlung</item>
<item quantity="other">%1$d NFTs in der %2$d Sammlung</item>
</plurals>
<string name="nft_wallet_receive_nft">Tippe hier, um das erste NFT zu erhalten</string>
@ -821,6 +821,7 @@
<string name="send_fee_unreachable_error_text">Überprüfe deine Netzwerkverbindung</string>
<string name="send_fee_unreachable_error_title">Informationen zur Netzwerkgebühr nicht erreichbar</string>
<string name="send_from_wallet_android">Von</string>
<string name="send_from_wallet_name">Von %s</string>
<string name="send_gas_limit">Grenzwert Gasgebühr</string>
<string name="send_gas_limit_footer">Dies ist die maximale Gasgebühr, die für den Abschluss einer Transaktion oder eines Vertrags ausgegeben wird. Ein Gaslimit verhindert unerwartete oder unbegrenzte Gebühren bei der Ausführung einer Transaktion.</string>
<string name="send_gas_price">Gaspreis</string>
@ -828,6 +829,7 @@
<string name="send_max_amount">Max</string>
<string name="send_max_amount_label">Höchstbetrag</string>
<string name="send_max_fee">Gebühr bis zu</string>
<string name="send_memo">Memo: %s</string>
<string name="send_memo_destination_tag_error">Ungültiges Memo</string>
<string name="send_network_fee_warning_title">Abdeckung der Netzgebühren</string>
<string name="send_nonce">Nonce</string>
@ -878,6 +880,7 @@
<string name="send_summary_transaction_description_suffix_fee_covered">Die Netzwerkgebühr wird durch die Nutzung von %1$s Energieträgern gedeckt.</string>
<string name="send_summary_transaction_description_suffix_fee_reduced">Die Netzwerkgebühr wird durch den Verbrauch von %1$s Energie reduziert</string>
<string name="send_summary_transaction_description_suffix_including">inklusive einer Netzgebühr von %1$s</string>
<string name="send_to_address">Zur Adresse</string>
<string name="send_transaction_success">Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert</string>
<string name="send_tron_account_activation_error">%1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen.</string>
<string name="send_validation_amount_exceeds_balance">Der Betrag geht über die Bilanz hinaus</string>
@ -887,6 +890,7 @@
<string name="send_validation_invalid_fee">Die Gebühr geht über die Bilanz hinaus</string>
<string name="send_validation_invalid_total">Der Gesamtbetrag geht über die Bilanz hinaus</string>
<string name="send_with_swap_notification_text">Sende uns ein Token, und wir konvertieren es unterwegs. Dein Empfänger erhält genau das, was er braucht nahtlos.</string>
<string name="send_with_swap_recipient_amount_text">Wird an den Empfänger gesendet</string>
<string name="send_with_swap_title">Senden mit Swap</string>
<string name="sent_transaction_sent_title">Transaktion gesendet</string>
<string name="settings_card_settings_footer">Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest.</string>
@ -1316,7 +1320,7 @@
<string name="wc_alert_unsupported_method_title">Wir haben einen unbekannten Fehler festgestellt.</string>
<string name="wc_alert_unsupported_networks_description">Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht.</string>
<string name="wc_alert_unsupported_networks_title">Nicht unterstützte Netzwerke</string>
<string name="wc_alert_verified_domain_description">Tangem unterstützt ein erforderliches Netzwerk um %s.</string>
<string name="wc_alert_verified_domain_description">Tangem unterstützt ein erforderliches Netzwerk um %s</string>
<string name="wc_alert_verified_domain_title">Verifizierte Domain</string>
<string name="wc_alert_wrong_card_description">Falsche Karte oder falscher Ring in der App ausgewählt</string>
<string name="wc_alert_wrong_card_title">Wir haben eine Art Problem</string>

View file

@ -560,7 +560,7 @@
<string name="nft_untitled_collection">Colección sin título</string>
<string name="nft_wallet_count">%1$d NFT en %2$d colecciones</string>
<plurals name="nft_wallet_count_android">
<item quantity="one"></item>
<item quantity="one">%1$d NFT en %2$d coleccione</item>
<item quantity="other">%1$d NFTs en %2$d colecciones</item>
</plurals>
<string name="nft_wallet_receive_nft">Pulse aquí para recibir el primer NFT</string>

View file

@ -561,7 +561,7 @@
<string name="nft_untitled_collection">Collection sans titre</string>
<string name="nft_wallet_count">%1$d NFT dans la collection %2$d</string>
<plurals name="nft_wallet_count_android">
<item quantity="one"></item>
<item quantity="one">%1$d NFT dans la collection %2$d</item>
<item quantity="other">%1$d NFT dans la collection %2$d</item>
</plurals>
<string name="nft_wallet_receive_nft">Appuyez ici pour recevoir le premier NFT</string>

View file

@ -1,5 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="access_code_alert_skip_description">アクセスコードがないとウォレットは保護されません。</string>
<string name="access_code_alert_skip_ok">とにかくスキップ</string>
<string name="access_code_alert_skip_title">アクセスコードが設定されていません</string>
<string name="access_code_check_title">アクセスコードを入力</string>
<string name="access_code_confirm_description">続行するには、以前に入力したコードを確認してください</string>
<string name="access_code_confirm_title">アクセスコードを再入力</string>
<string name="access_code_create_description">ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。</string>
<string name="access_code_create_title">アクセスコードの作成</string>
<string name="access_code_navtitle">アクセスコード</string>
<string name="action_buttons_buy_empty_search_message">トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して買付できるようにします。</string>
<string name="action_buttons_sell_empty_search_message">トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して売却できるようにします。</string>
<string name="action_buttons_sell_navigation_bar_title">売却</string>
@ -49,6 +58,17 @@
<string name="app_settings_theme_mode_system">システムのデフォルト</string>
<string name="app_settings_theme_selector_title">テーマ</string>
<string name="app_settings_title">アプリ設定</string>
<string name="backup_complete_description">ウォレットのバックアップが正常に完了しました。</string>
<string name="backup_complete_title">バックアップが完了しました</string>
<string name="backup_info_description">シークレットリカバリーフレーズは、ウォレットへのアクセスと復元のために使用される%sのランダムな単語のセットです。</string>
<string name="backup_info_keep_description">これらの単語は紛失した場合、復元できません。必ず安全な場所に保管してください。</string>
<string name="backup_info_keep_title">安全に保管してください</string>
<string name="backup_info_save_description">これらの%s個の単語をパスワードマネージャーなどの安全な場所に保存し、決して他の人と共有しないでください。</string>
<string name="backup_info_save_title">復元は不可能です</string>
<string name="backup_info_title">リカバリーフレーズ</string>
<string name="backup_seed_description">これらの%s語を順番に書き留めて、安全かつプライベートに保管してください。</string>
<string name="backup_seed_responsibility">ウォレットと、リカバリーフレーズのセキュリティとバックアップの全責任は、Tangemではなくユーザーにあります。</string>
<string name="backup_seed_title">リカバリーフレーズ</string>
<string name="balance_hidden_description">残高を表示または非表示にするには、デバイスの画面を下向きにするか、設定でオフにしてください。</string>
<string name="balance_hidden_do_not_show_button">今後表示しない</string>
<string name="balance_hidden_got_it_button">わかりました</string>
@ -101,6 +121,7 @@
<string name="common_approval">承認</string>
<string name="common_approve">承認</string>
<string name="common_attention">注意</string>
<string name="common_backup">バックアップ</string>
<string name="common_balance">残高: %s</string>
<string name="common_balance_title">残高</string>
<string name="common_biometric_authentication">生体認証</string>
@ -117,10 +138,12 @@
<string name="common_claim">請求</string>
<string name="common_claim_rewards">報酬を受け取る</string>
<string name="common_close">閉じる</string>
<string name="common_coming_soon">近日公開</string>
<string name="common_confirm">確認</string>
<string name="common_contact_tangem_support">Tangemサポートへ問い合わせる</string>
<string name="common_contact_visa_support">Visaサポートへ問い合わせる</string>
<string name="common_continue">続ける</string>
<string name="common_convert">変換する</string>
<string name="common_copy">コピー</string>
<string name="common_copy_address">アドレスをコピー</string>
<string name="common_create">作成</string>
@ -150,6 +173,7 @@
<string name="common_fee_selector_option_slow">遅い</string>
<string name="common_fee_selector_title">速度と料金</string>
<string name="common_finish">終了</string>
<string name="common_free">無料</string>
<string name="common_generate_addresses">アドレスを同期する</string>
<string name="common_go_to_provider">プロバイダーへ移動</string>
<string name="common_go_to_token">トークンへ移動</string>
@ -202,6 +226,7 @@
<string name="common_show_more">もっと見る</string>
<string name="common_sign">署名</string>
<string name="common_sign_and_send">署名して送信</string>
<string name="common_skip">スキップ</string>
<string name="common_stake">ステーキング</string>
<string name="common_staking">ステーキング</string>
<string name="common_start">始める</string>
@ -222,6 +247,7 @@
<string name="common_unreachable">アクセスできません</string>
<string name="common_unstake">ステーキング解除</string>
<string name="common_utxo_validate_withdrawal_message_warning">%1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。</string>
<string name="common_value_copied">値がコピーされました</string>
<string name="common_week"></string>
<string name="common_with"></string>
<string name="common_yes">はい</string>
@ -377,10 +403,24 @@
<string name="give_permission_title">許可を与える</string>
<string name="give_permission_unlimited">無制限</string>
<string name="home_button_add_existing_wallet">既存のウォレットを追加</string>
<string name="home_button_create_new_wallet">新しいウォレットを作成する</string>
<string name="home_button_order">Tangemを注文</string>
<string name="home_button_scan">Tangemをスキャン</string>
<string name="hot_crypto_add_token_subtitle">%sへ</string>
<string name="hot_crypto_token_network">%sネットワーク</string>
<string name="hw_backup_icloud_description">iCloudバックアップに保存されている既存のウォレットを復元する</string>
<string name="hw_backup_icloud_title">iCloudバックアップ</string>
<string name="hw_backup_need_action">バックアップへ移動</string>
<string name="hw_backup_need_description">アクセスコードを使用してウォレットを保護するには、まずバックアップを完了してください。</string>
<string name="hw_backup_need_title">まずバックアップを完了する</string>
<string name="hw_backup_no_backup">バックアップなし</string>
<string name="hw_backup_seed_description">暗号資産をオフラインで安全に保管する物理カード</string>
<string name="hw_backup_seed_title">リカバリーフレーズ</string>
<string name="hw_create_keys_description">受信取引の通知を受け取る</string>
<string name="hw_create_keys_title">鍵はアプリに保存されます</string>
<string name="hw_create_seed_description">最新の機能とニュースをお届けします</string>
<string name="hw_create_seed_title">シードフレーズのバックアップ</string>
<string name="hw_create_title">モバイルウォレットを作成する</string>
<string name="information_generated_with_ai">この情報はAIで生成されました。 \nエラーが見つかった場合は、ここをタップしてください。</string>
<string name="initial_message_change_access_code_body">アクセスコードを変更するには、上図のようにカードまたはリングをタップし、操作が終了するまで取り外さないでください。</string>
<string name="initial_message_change_passcode_body">パスコードを変更するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。</string>
@ -565,7 +605,7 @@
<string name="nft_receive_unavailable_asset_warning_title">ネットワークが追加されていません</string>
<string name="nft_receive_unavailable_section_title">未追加</string>
<string name="nft_receive_unsupported_types">サポートされていないNFTタイプ</string>
<string name="nft_receive_unsupported_types_description">cNFTとpNFTはまだサポートされていません</string>
<string name="nft_receive_unsupported_types_description">cNFTとpNFTはまだサポートされていません。ウォレットに送信しないでください。</string>
<string name="nft_send">NFTを送信する</string>
<string name="nft_traits_title">特徴</string>
<string name="nft_untitled_collection">無題のコレクション</string>
@ -621,6 +661,7 @@
<string name="onboarding_create_wallet_options_title">秘密鍵を非公開で生成する</string>
<string name="onboarding_done_body">カードは有効化され、使用可能になりました</string>
<string name="onboarding_done_header">成功!</string>
<string name="onboarding_done_wallet">ウォレットの設定が完了し、使用できるようになりました。</string>
<string name="onboarding_exit_alert_message">この場合は、最初からやり直す必要があります。</string>
<string name="onboarding_exit_alert_title">アクティベーション処理を途中で終了しますか?</string>
<string name="onboarding_getting_started">スタート</string>
@ -783,7 +824,9 @@
<string name="send_alert_fee_too_low_text">推奨手数料を下回る手数料が指定されたため、取引に遅れが生じる可能性があります。続行しますか?</string>
<string name="send_alert_transaction_failed_text">理由: %1$s \nコード: %2$s</string>
<string name="send_alert_transaction_failed_title">取引は完了していません</string>
<string name="send_amount_convert_to_another_token">別のトークンに変換する</string>
<string name="send_amount_label">金額</string>
<string name="send_amount_receive_token_subtitle">受取人に送信されます</string>
<string name="send_bitcoin_custom_fee_footer">取引手数料は、vByteフィールドのSatoshiの値を調整して設定できます。</string>
<string name="send_custom_amount_fee_footer">取引にかかる手数料です。自由に設定できます。</string>
<string name="send_custom_evm_max_fee">最大手数料</string>
@ -807,6 +850,7 @@
<string name="send_fee_unreachable_error_text">ネットワーク接続を確認してください</string>
<string name="send_fee_unreachable_error_title">ネットワーク手数料についての情報にアクセスできません</string>
<string name="send_from_wallet_android">より</string>
<string name="send_from_wallet_name">%sから</string>
<string name="send_gas_limit">ガス上限</string>
<string name="send_gas_limit_footer">これは、取引または契約を完了するために使用されるガスの最大額です。ガス上限を設定することで、取引実行時に予期せぬ請求や無制限の請求を防ぐことができます。</string>
<string name="send_gas_price">ガス代</string>
@ -814,6 +858,7 @@
<string name="send_max_amount">最大</string>
<string name="send_max_amount_label">最大金額</string>
<string name="send_max_fee">最大手数料</string>
<string name="send_memo">メモ: %s</string>
<string name="send_memo_destination_tag_error">無効なメモ</string>
<string name="send_network_fee_warning_title">ネットワーク手数料のカバー</string>
<string name="send_nonce">ナンス</string>
@ -827,6 +872,7 @@
<string name="send_notification_fee_too_high_title">カスタム手数料が高くなっています</string>
<string name="send_notification_high_fee_text">%1$sネットワークの特殊性により、残高全体を転送する場合の手数料は高くなります。手数料を削減するには、 %2$sを残します。</string>
<string name="send_notification_high_fee_title">手数料が高くなっています</string>
<string name="send_notification_invalid_amount_rent_destination">受取人の口座が有効化されていません。最低送金額はレント免除残高%1$s以上である必要があります。</string>
<string name="send_notification_invalid_amount_rent_fee">口座残高はレンタル料金より低くすることはできません。残高を少なくとも%1$sに維持するか、すべての資金を引き出してください。</string>
<string name="send_notification_invalid_amount_text">手数料が送金金額を超えており、マイナスの値になってしまいます。</string>
<string name="send_notification_invalid_amount_title">無効な金額</string>
@ -845,7 +891,7 @@
<string name="send_recent_transactions">最近の取引</string>
<string name="send_recipient">受取人</string>
<string name="send_recipient_address_error">有効なアドレスではありません</string>
<string name="send_recipient_address_footer">トークンを失わないように、受信ウォレットアドレスが%sネットワーク上にあることを確認してください。</string>
<string name="send_recipient_address_footer">トークンの損失を避けるために、受信ウォレットアドレスが** %sネットワーク**上にあることを確認してください</string>
<string name="send_recipient_address_footer_highlighted_part">%s に送信</string>
<string name="send_recipient_address_footer_v2">%sネットワークアドレスを確認してください。エラーが発生すると転送が失われる可能性があります。</string>
<string name="send_recipient_label">送金先</string>
@ -864,6 +910,7 @@
<string name="send_summary_transaction_description_suffix_fee_covered">ネットワーク手数料は%1$sエネルギーを使用してカバーされます</string>
<string name="send_summary_transaction_description_suffix_fee_reduced">%1$sエネルギーを使用するとネットワーク手数料が減額されます</string>
<string name="send_summary_transaction_description_suffix_including">ネットワーク手数料%1$sを含む</string>
<string name="send_to_address">アドレスへ</string>
<string name="send_transaction_success">取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。</string>
<string name="send_tron_account_activation_error">%1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTronTRXを入金する必要があります。</string>
<string name="send_validation_amount_exceeds_balance">金額が残高を超えています</string>
@ -873,6 +920,8 @@
<string name="send_validation_invalid_fee">手数料が残高を超えています</string>
<string name="send_validation_invalid_total">合計金額が残高を超えています</string>
<string name="send_with_swap_notification_text">トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。</string>
<string name="send_with_swap_recipient_amount_text">受信者に送信されます</string>
<string name="send_with_swap_recipient_amount_title">受取金額</string>
<string name="send_with_swap_title">スワップして送信</string>
<string name="sent_transaction_sent_title">取引が送信されました</string>
<string name="settings_card_settings_footer">設定したいカードまたはリングをスキャンするために準備してください。</string>
@ -1028,6 +1077,7 @@
<string name="swap_story_second_title">破格のレート</string>
<string name="swap_story_third_subtitle">手間がかからず直感的に操作でき、数回タップするだけでトークンを交換できます。</string>
<string name="swap_story_third_title">とにかく便利</string>
<string name="swap_via_provider">プロバイダー経由のスワップ</string>
<string name="swapping_alert_cex_description">この金額には以下が含まれます:\n- サービスプロバイダーの手数料\n- 取引所からユーザーのアドレスに%s を送り返すためのネットワーク手数料。</string>
<string name="swapping_alert_cex_description_with_slippage">金額には以下が含まれます: \n • サービス プロバイダーの手数料\n • 取引所からユーザーのアドレスに%1$sを送金するためのネットワーク手数料。 \n\nプロバイダーのスリッページは最大%2$sです</string>
<string name="swapping_alert_dex_description">この金額には、サービスプロバイダーの手数料が含まれています。</string>
@ -1178,6 +1228,29 @@
<string name="wallet_connect_subtitle">dAppsに接続する</string>
<string name="wallet_connect_title">WalletConnect</string>
<string name="wallet_connect_toast_awaiting_session_proposal">接続には数秒かかる場合があります</string>
<string name="wallet_create_hardware_badge">%sから</string>
<string name="wallet_create_hardware_description">Tangemウォレットを購入 — 暗号資産をオフラインで安全に保管できる物理カードです。</string>
<string name="wallet_create_hardware_title">ハードウェアウォレット</string>
<string name="wallet_create_mobile_description">数秒でスマートフォンに安全なウォレットが作成されます。</string>
<string name="wallet_create_mobile_title">モバイルウォレット</string>
<string name="wallet_create_nav_info_title">何を選ぶべき?</string>
<string name="wallet_create_scan_question">すでにTangemウォレットをお持ちですか</string>
<string name="wallet_create_scan_title">今すぐスキャン</string>
<string name="wallet_create_title">ウォレットの作成方法を選択してください</string>
<string name="wallet_import_buy_question">Tangemウォレットを購入しますか</string>
<string name="wallet_import_buy_title">今すぐ購入</string>
<string name="wallet_import_google_drive_description">Googleドライブのバックアップに保存されている既存のウォレットを復元する</string>
<string name="wallet_import_google_drive_title">Googleドライブからインポート</string>
<string name="wallet_import_navtitle">既存のウォレットを追加</string>
<string name="wallet_import_scan_description">暗号資産をオフラインで安全に保管する物理カード。</string>
<string name="wallet_import_scan_title">Tangemウォレットをスキャンする</string>
<string name="wallet_import_seed_description">ウォレットを復元するためのシードフレーズ</string>
<string name="wallet_import_seed_navtitle">ウォレットをインポート</string>
<string name="wallet_import_seed_title">リカバリーフレーズをインポートする</string>
<string name="wallet_import_success_description">ウォレットのバックアップが正常に完了しました。</string>
<string name="wallet_import_success_navtitle">ウォレットをインポート</string>
<string name="wallet_import_success_title">インポート完了</string>
<string name="wallet_import_title">ウォレットをインポート</string>
<string name="wallet_marketplace_block_title">%s市場価格</string>
<string name="wallet_marketprice_block_update_time">直近24時間</string>
<string name="wallet_network_group_title">%sネットワーク</string>
@ -1186,6 +1259,8 @@
<string name="wallet_promo_banner_button_title">今すぐ10 % オフで購入</string>
<string name="wallet_promo_banner_description">1.3万種類以上の暗号資産にアクセス。ワンタップで買付、売却、スワップ、ステーキングが可能です。\nバックアップ用に最大3枚のカードを連携できます。</string>
<string name="wallet_promo_banner_title">Tangemウォレットを見る</string>
<string name="wallet_settings_access_code_description">このウォレットを保護するためのシークレットコードです。ログインと署名に使用されます。</string>
<string name="wallet_settings_access_code_title">アクセスコードの設定 / 変更</string>
<string name="wallet_settings_push_notifications_description">ウォレットの受信取引とTangemの更新について通知を受け取る。</string>
<string name="wallet_settings_push_notifications_title">取引通知</string>
<string name="wallet_settings_title">ウォレット設定</string>
@ -1274,6 +1349,10 @@
<string name="warning_some_networks_unreachable_message">下にスワイプして更新するか、後でもう一度お試しください。</string>
<string name="warning_some_networks_unreachable_title">一部のネットワークにアクセスできません</string>
<string name="warning_some_token_balances_not_updated">一部のトークン残高を更新できませんでした</string>
<string name="warning_stellar_token_trustline_button_title">トラストラインを有効にする</string>
<string name="warning_stellar_token_trustline_not_enough_xlm">%sが足りません。このトークンを関連付けるには、XLMアカウントに資金を追加してください。</string>
<string name="warning_stellar_token_trustline_subtitle">このトークンを受け取るには、トラストラインを有効にする必要があります。ネットワークには 0.5 XLMの準備金が必要です。</string>
<string name="warning_stellar_token_trustline_title">トラストラインが必要</string>
<string name="warning_testnet_card_message">これはテストネットカードです。取引処理はできませんので、テストおよび開発目的でのみご利用ください。</string>
<string name="warning_testnet_card_title">テスト目的のみ</string>
<string name="warning_token_balance_not_updated">残高が古い可能性があります。ページを更新してください。</string>
@ -1296,7 +1375,7 @@
<string name="wc_alert_unsupported_method_title">不明なエラーが発生しました</string>
<string name="wc_alert_unsupported_networks_description">Tangemは現在%sで必要なネットワークをサポートしていません。</string>
<string name="wc_alert_unsupported_networks_title">サポートされていないネットワーク</string>
<string name="wc_alert_verified_domain_description">Tangemは%sで必要なネットワークをサポートします</string>
<string name="wc_alert_verified_domain_description">Tangemは%sで必要なネットワークをサポートします</string>
<string name="wc_alert_verified_domain_title">検証済みドメイン</string>
<string name="wc_alert_wrong_card_description">アプリで間違ったカードまたはリングが選択されました</string>
<string name="wc_alert_wrong_card_title">問題が起きています</string>

View file

@ -223,6 +223,7 @@
<string name="common_unreachable">Недоступно</string>
<string name="common_unstake">Завершить стейкинг</string>
<string name="common_utxo_validate_withdrawal_message_warning">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string>
<string name="common_value_copied">Значение скопировано</string>
<string name="common_week">неделю</string>
<string name="common_with">с</string>
<string name="common_yes">Да</string>
@ -843,6 +844,7 @@
<string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string>
<string name="send_notification_high_fee_text">Ввиду особенности сети %1$s комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %2$s.</string>
<string name="send_notification_high_fee_title">Комиссия повышена</string>
<string name="send_notification_invalid_amount_rent_destination">Аккаунт получателя не активирован. Минимальная сумма перевода должна быть не меньше баланса, необходимого для покрытия арендной платы: %1$s.</string>
<string name="send_notification_invalid_amount_rent_fee">Баланс вашего счета не может быть ниже арендной платы. Пожалуйста, оставьте на счете не менее %1$s или выведите все средства.</string>
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string>
<string name="send_notification_invalid_amount_title">Недопустимая сумма</string>
@ -1259,6 +1261,9 @@
<string name="warning_some_networks_unreachable_message">Свайпните вниз для обновления или попробуйте позже.</string>
<string name="warning_some_networks_unreachable_title">Некоторые сети недоступны</string>
<string name="warning_some_token_balances_not_updated">Некоторые балансы токенов не удалось обновить</string>
<string name="warning_stellar_token_trustline_button_title">Открыть Trustline</string>
<string name="warning_stellar_token_trustline_subtitle">Чтобы получить этот токен, необходимо открыть Trustline. Сеть удержит резерв в размере 0.5 XLM.</string>
<string name="warning_stellar_token_trustline_title">Откройте Trustline</string>
<string name="warning_testnet_card_message">Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки.</string>
<string name="warning_testnet_card_title">Только для целей тестирования</string>
<string name="warning_token_balance_not_updated">Возможно, баланс устарел. Обновите страницу.</string>

View file

@ -574,7 +574,7 @@
<string name="nft_traits_title">Риси</string>
<string name="nft_wallet_count">NFTs в %2$d колекціях%1$d</string>
<plurals name="nft_wallet_count_android">
<item quantity="one"></item>
<item quantity="one">%1$d NFT в %2$d колекціi</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other">%1$d NFTs в %2$d колекціях</item>

View file

@ -1,5 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="access_code_alert_skip_description">Your wallet wont be protected without an Access Code.</string>
<string name="access_code_alert_skip_ok">Skip anyway</string>
<string name="access_code_alert_skip_title">Access Code not set</string>
<string name="access_code_check_title">Enter Access Code</string>
<string name="access_code_confirm_description">Confirm your previously entered code to continue</string>
<string name="access_code_confirm_title">Re-enter Access Code</string>
<string name="access_code_create_description">Set a %s-digit Access Code to unlock your wallet.</string>
<string name="access_code_create_title">Create Access Code</string>
<string name="access_code_navtitle">Access code</string>
<string name="action_buttons_buy_empty_search_message">Cant find your token? Go to the Market section on the main page and add it to your portfolio for purchase</string>
<string name="action_buttons_sell_empty_search_message">Cant find your token? Go to the Market section on the main page and add it to your portfolio for selling.</string>
<string name="action_buttons_sell_navigation_bar_title">Sell</string>
@ -49,6 +58,17 @@
<string name="app_settings_theme_mode_system">System default</string>
<string name="app_settings_theme_selector_title">Theme</string>
<string name="app_settings_title">App settings</string>
<string name="backup_complete_description">You successfully backed up your wallet.</string>
<string name="backup_complete_title">Backup Completed</string>
<string name="backup_info_description">Your Secret Recovery Phrase is a fixed set of %s random words used to access and recover your wallet.</string>
<string name="backup_info_keep_description">These words cant be recovered if lost. Make sure to keep it somewhere secure.</string>
<string name="backup_info_keep_title">Keep It Safe</string>
<string name="backup_info_save_description">Save these %s words in a secure location, such as a password manager, and never share them with anyone.</string>
<string name="backup_info_save_title">No Recovery Possible</string>
<string name="backup_info_title">Recovery phrase</string>
<string name="backup_seed_description">Write down these %s words in order and keep them safe and private</string>
<string name="backup_seed_responsibility">Full responsibility for the security and backup of the wallet and recovery phrase lies with the user, not with Tangem.</string>
<string name="backup_seed_title">Recovery phrase</string>
<string name="balance_hidden_description">To hide or show your balances, simply flip your device screen down, or switch it off in Settings</string>
<string name="balance_hidden_do_not_show_button">Don\'t show again</string>
<string name="balance_hidden_got_it_button">Got it</string>
@ -103,6 +123,7 @@
<string name="common_approval">Approval</string>
<string name="common_approve">Approve</string>
<string name="common_attention">Attention</string>
<string name="common_backup">Backup</string>
<string name="common_balance">Balance: %s</string>
<string name="common_balance_title">Balance</string>
<string name="common_biometric_authentication">biometric authentication</string>
@ -124,6 +145,7 @@
<string name="common_contact_tangem_support">Contact Tangem Support</string>
<string name="common_contact_visa_support">Contact Visa Support</string>
<string name="common_continue">Continue</string>
<string name="common_convert">Convert</string>
<string name="common_copy">Copy</string>
<string name="common_copy_address">Copy address</string>
<string name="common_create">Create</string>
@ -208,6 +230,7 @@
<string name="common_show_more">Show more</string>
<string name="common_sign">Sign</string>
<string name="common_sign_and_send">Sign and send</string>
<string name="common_skip">Skip</string>
<string name="common_stake">Stake</string>
<string name="common_staking">Staking</string>
<string name="common_start">Start</string>
@ -229,6 +252,7 @@
<string name="common_unreachable">Unreachable</string>
<string name="common_unstake">Unstake</string>
<string name="common_utxo_validate_withdrawal_message_warning">Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.</string>
<string name="common_value_copied">Value copied</string>
<string name="common_week">week</string>
<string name="common_with">with</string>
<string name="common_yes">Yes</string>
@ -596,7 +620,7 @@
<string name="nft_untitled_collection">Untitled collection</string>
<string name="nft_wallet_count">%1$d NFTs in %2$d collections</string>
<plurals name="nft_wallet_count_android">
<item quantity="one"></item>
<item quantity="one">%1$d NFT in %2$d collection</item>
<item quantity="other">%1$d NFTs in %2$d collections</item>
</plurals>
<string name="nft_wallet_receive_nft">Tap here to receive first NFT</string>
@ -647,6 +671,7 @@
<string name="onboarding_create_wallet_options_title">Generate keys privately</string>
<string name="onboarding_done_body">Your card is activated and ready to be used</string>
<string name="onboarding_done_header">Success!</string>
<string name="onboarding_done_wallet">Your wallet is set up and ready to use!</string>
<string name="onboarding_exit_alert_message">In this case, you will need to start from the beginning.</string>
<string name="onboarding_exit_alert_title">Do you want to exit the activation process?</string>
<string name="onboarding_getting_started">Getting started</string>
@ -813,7 +838,9 @@
<string name="send_alert_fee_too_low_text">You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue?</string>
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode: %2$s</string>
<string name="send_alert_transaction_failed_title">The transaction is not completed</string>
<string name="send_amount_convert_to_another_token">Convert to another token</string>
<string name="send_amount_label">Amount</string>
<string name="send_amount_receive_token_subtitle">Will be sent to recipient</string>
<string name="send_bitcoin_custom_fee_footer">You can set your transaction fee by adjusting the value in the Satoshi per vByte field.</string>
<string name="send_custom_amount_fee_footer">The fee that will be charged for your transaction. You can set your own value.</string>
<string name="send_custom_evm_max_fee">Max fee</string>
@ -859,6 +886,7 @@
<string name="send_notification_fee_too_high_title">Custom fee is high</string>
<string name="send_notification_high_fee_text">Due to the peculiarities of the %1$s network, the fee for transferring the entire balance is higher. To reduce the commission, you can leave %2$s.</string>
<string name="send_notification_high_fee_title">The fee is higher</string>
<string name="send_notification_invalid_amount_rent_destination">The recipient account is not activated. The minimum transfer amount must be equal to or greater than the rent-exempt balance: %1$s.</string>
<string name="send_notification_invalid_amount_rent_fee">Your account balance cannot be lower than the rent fee. Please maintain at least %1$s on your account or withdraw all funds.</string>
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value</string>
<string name="send_notification_invalid_amount_title">Invalid amount</string>
@ -877,7 +905,7 @@
<string name="send_recent_transactions">Recent</string>
<string name="send_recipient">Recipient</string>
<string name="send_recipient_address_error">Not a valid address</string>
<string name="send_recipient_address_footer">Ensure the receiving wallet address is on the %s network to avoid losing your tokens</string>
<string name="send_recipient_address_footer">Ensure the receiving wallet address is on **the %s network** to avoid losing your tokens</string>
<string name="send_recipient_address_footer_highlighted_part">send to %s</string>
<string name="send_recipient_address_footer_v2">Ensure you %s network address, as errors may result in lost transfers</string>
<string name="send_recipient_label">Send to</string>
@ -907,6 +935,7 @@
<string name="send_validation_invalid_total">Total amount exceeds balance</string>
<string name="send_with_swap_notification_text">Send any token, and well convert it on the way. Your recipient gets exactly what they need—seamlessly.</string>
<string name="send_with_swap_recipient_amount_text">Will be sent a recipient</string>
<string name="send_with_swap_recipient_amount_title">Amount to receive</string>
<string name="send_with_swap_title">Send with swap</string>
<string name="sent_transaction_sent_title">Transaction sent</string>
<string name="settings_card_settings_footer">Prepare to scan card or ring you want to set up.</string>
@ -1062,6 +1091,7 @@
<string name="swap_story_second_title">Unbeatable Rates</string>
<string name="swap_story_third_subtitle">Hassle-free and intuitive, allowing you to swap tokens in just a few taps</string>
<string name="swap_story_third_title">Simply Convenient</string>
<string name="swap_via_provider">Swap via provider</string>
<string name="swapping_alert_cex_description">The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address.</string>
<string name="swapping_alert_cex_description_with_slippage">The amount includes:\n• service provider\'s fee\n• network fee for sending %1$s from the exchange back to the user\'s address. \n\nProvider slippage is up to %2$s</string>
<string name="swapping_alert_dex_description">The amount includes the service provider\'s fee.</string>
@ -1292,6 +1322,8 @@
<string name="wallet_promo_banner_button_title">Get now with 10% off</string>
<string name="wallet_promo_banner_description">Access 13,000+ cryptocurrencies. Buy, sell, swap, and stake with a single tap.\nLink up to three cards for a backup.</string>
<string name="wallet_promo_banner_title">Discover Tangem Wallet</string>
<string name="wallet_settings_access_code_description">Secret code to protect this wallet. Used for login and signatures.</string>
<string name="wallet_settings_access_code_title">Set/Change Access Code</string>
<string name="wallet_settings_push_notifications_description">Stay notified on wallet incoming transactions and Tangem updates.</string>
<string name="wallet_settings_push_notifications_title">Transaction Notifications</string>
<string name="wallet_settings_title">Wallet settings</string>
@ -1381,6 +1413,10 @@
<string name="warning_some_networks_unreachable_message">Swipe down to refresh or try again later.</string>
<string name="warning_some_networks_unreachable_title">Some networks are unreachable</string>
<string name="warning_some_token_balances_not_updated">Some token balances could not be updated</string>
<string name="warning_stellar_token_trustline_button_title">Enable Trustline</string>
<string name="warning_stellar_token_trustline_not_enough_xlm">Not enough %s. Top up your XLM account to associate this token</string>
<string name="warning_stellar_token_trustline_subtitle">A Trustline must be enabled to receive this token. The network requires a 0.5 XLM reserve.</string>
<string name="warning_stellar_token_trustline_title">Trustline Required</string>
<string name="warning_testnet_card_message">This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes.</string>
<string name="warning_testnet_card_title">For testing purposes only</string>
<string name="warning_token_balance_not_updated">Balance may be outdated. Refresh the page.</string>
@ -1403,7 +1439,7 @@
<string name="wc_alert_unsupported_method_title">We\'ve encountered unknown error</string>
<string name="wc_alert_unsupported_networks_description">Tangem does not currently support a required network by %s.</string>
<string name="wc_alert_unsupported_networks_title">Unsuported networks</string>
<string name="wc_alert_verified_domain_description">Tangem support a required network by %s.</string>
<string name="wc_alert_verified_domain_description">Tangem support a required network by %s</string>
<string name="wc_alert_verified_domain_title">Verified domain</string>
<string name="wc_alert_wrong_card_description">Wrong card or ring selected in the App</string>
<string name="wc_alert_wrong_card_title">We\'ve got some kind of problem</string>

View file

@ -39,11 +39,8 @@ internal class SendAmountBlockComponent(
val isClickEnabled by params.blockClickEnableFlow.collectAsStateWithLifecycle()
if (params.isRedesignEnabled) {
val amountState = state as? AmountState.Data ?: return
AmountBlockV2(
amountState = state,
currencyIconState = amountState.tokenIconState,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
onClick = onClick,

View file

@ -0,0 +1,104 @@
package com.tangem.features.swap.v2.impl.amount
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams.AmountBlockParams
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountModel
import com.tangem.features.swap.v2.impl.amount.ui.SwapAmountBlockContent
import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
internal class SwapAmountBlockComponent(
private val appComponentContext: AppComponentContext,
private val params: AmountBlockParams,
private val onResult: (SwapAmountUM) -> Unit,
val onClick: () -> Unit,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: SwapAmountModel = getOrCreateModel(params = params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = null,
handleBackButton = true,
childFactory = ::bottomSheetChild,
)
init {
model.uiState.onEach {
onResult(it)
}.launchIn(componentScope)
}
fun updateState(amountUM: SwapAmountUM) = model.updateState(amountUM)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
val isClickEnabled by params.blockClickEnableFlow.collectAsStateWithLifecycle()
SwapAmountBlockContent(
amountUM = state,
onInfoClick = model::onInfoClick,
isClickEnabled = isClickEnabled,
onClick = onClick,
onProviderSelectClick = {
val amountUM = model.uiState.value as? SwapAmountUM.Content ?: return@SwapAmountBlockContent
val selectedProvider = amountUM.selectedQuote.provider ?: return@SwapAmountBlockContent
val cryptoCurrency = params.secondaryCryptoCurrency ?: return@SwapAmountBlockContent
model.bottomSheetNavigation.activate(
SwapChooseProviderConfig(
providers = amountUM.swapQuotes,
cryptoCurrency = cryptoCurrency,
selectedProvider = selectedProvider,
),
)
},
)
bottomSheet.child?.instance?.BottomSheet()
}
private fun bottomSheetChild(
config: SwapChooseProviderConfig,
componentContext: ComponentContext,
): ComposableBottomSheetComponent {
return SwapChooseProviderComponent(
context = childByContext(componentContext),
params = SwapChooseProviderComponent.Params(
providers = config.providers,
cryptoCurrency = config.cryptoCurrency,
selectedProvider = config.selectedProvider,
callback = model,
onDismiss = { model.bottomSheetNavigation.dismiss() },
),
)
}
data class SwapChooseProviderConfig(
val providers: ImmutableList<SwapQuoteUM>,
val cryptoCurrency: CryptoCurrency,
val selectedProvider: ExpressProvider,
)
}

View file

@ -1,6 +1,5 @@
package com.tangem.features.swap.v2.impl.amount
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -15,19 +14,19 @@ internal sealed class SwapAmountComponentParams {
abstract val amountUM: SwapAmountUM
abstract val analyticsCategoryName: String
abstract val userWallet: UserWallet
abstract val appCurrency: AppCurrency
abstract val swapDirection: SwapDirection
abstract val isBalanceHidingFlow: StateFlow<Boolean>
abstract val primaryCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
abstract val secondaryCryptoCurrency: CryptoCurrency?
data class AmountParams(
override val amountUM: SwapAmountUM,
override val analyticsCategoryName: String,
override val userWallet: UserWallet,
override val isBalanceHidingFlow: StateFlow<Boolean>,
override val appCurrency: AppCurrency,
override val swapDirection: SwapDirection,
val primaryCryptoCurrency: CryptoCurrency,
val secondaryCryptoCurrency: CryptoCurrency?,
override val primaryCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
override val secondaryCryptoCurrency: CryptoCurrency?,
val callback: SwapAmountComponent.ModelCallback,
val currentRoute: Flow<SwapRoute.Amount>,
) : SwapAmountComponentParams()
@ -37,10 +36,9 @@ internal sealed class SwapAmountComponentParams {
override val analyticsCategoryName: String,
override val userWallet: UserWallet,
override val isBalanceHidingFlow: StateFlow<Boolean>,
override val appCurrency: AppCurrency,
override val swapDirection: SwapDirection,
val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?,
override val primaryCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
override val secondaryCryptoCurrency: CryptoCurrency?,
val blockClickEnableFlow: StateFlow<Boolean>,
) : SwapAmountComponentParams()
}

View file

@ -34,8 +34,8 @@ internal sealed class SwapAmountUM {
override val primaryAmount: SwapAmountFieldUM,
override val secondaryAmount: SwapAmountFieldUM,
override val selectedAmountType: SwapAmountType,
val primaryCryptoCurrencyStatus: CryptoCurrencyStatus?,
val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?,
val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
// selected swap route
val swapDirection: SwapDirection,
@ -61,7 +61,7 @@ sealed class SwapAmountFieldUM {
) : SwapAmountFieldUM() {
override val amountField: AmountState = AmountState.Empty(
isPrimaryButtonEnabled = false,
isRedesignEnabled = false,
isRedesignEnabled = true,
)
}
@ -70,7 +70,7 @@ sealed class SwapAmountFieldUM {
) : SwapAmountFieldUM() {
override val amountField: AmountState = AmountState.Empty(
isPrimaryButtonEnabled = false,
isRedesignEnabled = false,
isRedesignEnabled = true,
)
}

View file

@ -0,0 +1,64 @@
package com.tangem.features.swap.v2.impl.amount.model
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.features.swap.v2.impl.R
import com.tangem.utils.StringsSigns.PERCENT
import javax.inject.Inject
@ModelScoped
internal class SwapAlertFactory @Inject constructor(
private val uiMessageSender: UiMessageSender,
) {
fun priceImpactAlert(hasPriceImpact: Boolean, currencySymbol: String, provider: ExpressProvider) {
val slippage = provider.slippage?.let { "${it.parseBigDecimal(1)}$PERCENT" }
val combinedMessage = buildList {
when (provider.type) {
ExpressProviderType.CEX -> {
if (slippage != null) {
add(
resourceReference(
id = R.string.swapping_alert_cex_description_with_slippage,
formatArgs = wrappedList(currencySymbol, slippage),
),
)
} else {
add(resourceReference(R.string.swapping_alert_cex_description, wrappedList(currencySymbol)))
}
}
ExpressProviderType.DEX,
ExpressProviderType.DEX_BRIDGE,
-> {
if (hasPriceImpact) {
add(resourceReference(R.string.swapping_high_price_impact_description))
add(stringReference("\n\n"))
}
if (slippage != null) {
add(
resourceReference(
id = R.string.swapping_alert_dex_description_with_slippage,
formatArgs = wrappedList(slippage),
),
)
} else {
add(resourceReference(R.string.swapping_alert_dex_description, wrappedList(currencySymbol)))
}
}
else -> Unit
}
}
uiMessageSender.send(
DialogMessage(
title = resourceReference(R.string.swapping_alert_title),
message = combinedReference(combinedMessage.toWrappedList()),
firstActionBuilder = { okAction() },
),
)
}
}

View file

@ -6,5 +6,6 @@ import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
internal interface SwapAmountClickIntents : AmountScreenClickIntents {
fun onExpandEditField(selectedAmountType: SwapAmountType)
fun onInfoClick()
fun onSelectTokenClick()
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.swap.v2.impl.amount.model
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
@ -14,10 +15,11 @@ import com.tangem.core.ui.extensions.resourceReference
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.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.swap.models.SwapQuoteModel
import com.tangem.domain.swap.usecase.GetSwapPairsUseCase
@ -28,6 +30,7 @@ import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.GetAllowanceUseCase
import com.tangem.features.swap.v2.impl.R
import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent.SwapChooseProviderConfig
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
@ -41,12 +44,10 @@ import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.properties.Delegates
import com.tangem.utils.transformer.update as transformerUpdate
@ -63,47 +64,41 @@ internal class SwapAmountModel @Inject constructor(
private val getSwapQuoteUseCase: GetSwapQuoteUseCase,
private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener,
private val getAllowanceUseCase: GetAllowanceUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val appRouter: AppRouter,
private val swapAlertFactory: SwapAlertFactory,
) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback {
private val params: SwapAmountComponentParams = paramsContainer.require()
private val swapDirection = params.swapDirection
private val appCurrency = params.appCurrency
private var appCurrency = AppCurrency.Default
private var userWallet = params.userWallet
private var primaryCryptoCurrency: CryptoCurrency by Delegates.notNull()
private var secondaryCryptoCurrency: CryptoCurrency? = null
private var primaryCryptoCurrency: CryptoCurrency = params.primaryCryptoCurrencyStatusFlow.value.currency
private var secondaryCryptoCurrency: CryptoCurrency? = params.secondaryCryptoCurrency
private var primaryCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var secondaryCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var primaryCryptoCurrencyStatus: CryptoCurrencyStatus = params.primaryCryptoCurrencyStatusFlow.value
private var secondaryCryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = primaryCryptoCurrencyStatus.currency,
value = CryptoCurrencyStatus.Loading,
)
private var primaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
private var secondaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
private var primaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
private var secondaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
val bottomSheetNavigation: SlotNavigation<SwapChooseProviderConfig> = SlotNavigation()
val uiState: StateFlow<SwapAmountUM>
field = MutableStateFlow(params.amountUM)
init {
when (params) {
is SwapAmountComponentParams.AmountParams -> {
primaryCryptoCurrency = params.primaryCryptoCurrency
secondaryCryptoCurrency = params.secondaryCryptoCurrency
}
is SwapAmountComponentParams.AmountBlockParams -> {
primaryCryptoCurrency = params.primaryCryptoCurrencyStatus.currency
secondaryCryptoCurrency = params.secondaryCryptoCurrencyStatus?.currency
primaryCryptoCurrencyStatus = params.primaryCryptoCurrencyStatus
params.secondaryCryptoCurrencyStatus?.let {
secondaryCryptoCurrencyStatus = it
}
}
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
}
initialState()
configAmountNavigation()
subscribeOnCryptoCurrencyStatusFlow()
observeChooseSelectToken()
// todo observe balance hiding flow
}
@ -116,8 +111,6 @@ internal class SwapAmountModel @Inject constructor(
uiState.transformerUpdate(
SwapAmountSelectQuoteTransformer(
quoteUM = quoteUM,
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary,
),
@ -133,11 +126,21 @@ internal class SwapAmountModel @Inject constructor(
}
}
override fun onInfoClick() {
val amountUM = uiState.value as? SwapAmountUM.Content ?: return
val selectedProvider = amountUM.selectedQuote.provider ?: return
val cryptoCurrency = secondaryCryptoCurrency ?: return
swapAlertFactory.priceImpactAlert(
hasPriceImpact = (amountUM.secondaryAmount as? SwapAmountFieldUM.Content)?.priceImpact != null,
currencySymbol = cryptoCurrency.symbol,
provider = selectedProvider,
)
}
override fun onAmountValueChange(value: String) {
uiState.transformerUpdate(
SwapAmountValueChangeTransformer(
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
primaryMaximumAmountBoundary = primaryMaximumAmountBoundary,
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
primaryMinimumAmountBoundary = primaryMinimumAmountBoundary,
@ -155,8 +158,6 @@ internal class SwapAmountModel @Inject constructor(
override fun onMaxValueClick() {
uiState.transformerUpdate(
SwapAmountValueMaxTransformer(
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
primaryMaximumAmountBoundary = primaryMaximumAmountBoundary,
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
primaryMinimumAmountBoundary = primaryMinimumAmountBoundary,
@ -169,8 +170,6 @@ internal class SwapAmountModel @Inject constructor(
override fun onCurrencyChangeClick(isFiat: Boolean) {
uiState.transformerUpdate(
SwapAmountChangeCurrencyTransformer(
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
isFiatSelected = isFiat,
),
)
@ -190,6 +189,27 @@ internal class SwapAmountModel @Inject constructor(
)
}
private fun subscribeOnCryptoCurrencyStatusFlow() {
params.primaryCryptoCurrencyStatusFlow.onEach { primaryCurrencyStatus ->
primaryCryptoCurrencyStatus = primaryCurrencyStatus
val state = uiState.value
if (state is SwapAmountUM.Content) {
secondaryCryptoCurrency = state.primaryCryptoCurrencyStatus.currency
secondaryCryptoCurrencyStatus = state.primaryCryptoCurrencyStatus
initCurrencies(
primaryStatus = state.primaryCryptoCurrencyStatus,
secondaryStatus = state.secondaryCryptoCurrencyStatus,
)
} else {
initPairs(
primaryCryptoCurrency = primaryCryptoCurrency,
secondaryCryptoCurrency = secondaryCryptoCurrency,
)
}
}.launchIn(modelScope)
}
private fun observeChooseSelectToken() {
swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow
.onEach { currency ->
@ -210,46 +230,12 @@ internal class SwapAmountModel @Inject constructor(
.launchIn(modelScope)
}
private fun initialState() {
if (uiState.value is SwapAmountUM.Empty) {
initPairs(
primaryCryptoCurrency = primaryCryptoCurrency,
secondaryCryptoCurrency = secondaryCryptoCurrency,
)
uiState.update {
SwapAmountUM.Content(
isPrimaryButtonEnabled = it.isPrimaryButtonEnabled,
primaryAmount = it.primaryAmount,
secondaryAmount = it.secondaryAmount,
swapDirection = swapDirection,
swapCurrencies = SwapCurrencies.EMPTY,
appCurrency = appCurrency,
swapQuotes = persistentListOf(),
swapRateType = ExpressRateType.Float,
selectedAmountType = SwapAmountType.From,
selectedQuote = SwapQuoteUM.Empty,
primaryCryptoCurrencyStatus = null,
secondaryCryptoCurrencyStatus = null,
)
}
}
}
private fun initPairs(primaryCryptoCurrency: CryptoCurrency, secondaryCryptoCurrency: CryptoCurrency?) {
modelScope.launch {
val cryptoCurrencyStatusList = getMultiCryptoCurrencyStatusUseCase
.invokeMultiWalletSync(userWallet.walletId)
.getOrElse { emptyList() }
val primaryStatus = cryptoCurrencyStatusList.firstOrNull {
it.currency.id == primaryCryptoCurrency.id
}
if (primaryStatus == null) {
Timber.e("Failed to get crypto currency status")
// todo error
return@launch
}
val cryptoCurrencyStatusListExceptPrimary = cryptoCurrencyStatusList.filter {
val statusFilter = it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount
val notCustomTokenFilter = !it.currency.isCustom
@ -271,7 +257,9 @@ internal class SwapAmountModel @Inject constructor(
)
if (secondaryStatus != null) {
initCurrencies(primaryStatus, secondaryStatus)
initCurrencies(primaryCryptoCurrencyStatus, secondaryStatus)
this@SwapAmountModel.secondaryCryptoCurrency = secondaryCryptoCurrency
secondaryCryptoCurrencyStatus = secondaryStatus
uiState.update {
SwapAmountReadyStateConverter(
swapCurrencies = swapCurrencies,
@ -296,9 +284,6 @@ internal class SwapAmountModel @Inject constructor(
}
private suspend fun initCurrencies(primaryStatus: CryptoCurrencyStatus, secondaryStatus: CryptoCurrencyStatus) {
primaryCryptoCurrencyStatus = primaryStatus
secondaryCryptoCurrencyStatus = secondaryStatus
primaryMinimumAmountBoundary = EnterAmountBoundary(
amount = getMinimumTransactionAmountSyncUseCase
.invoke(
@ -325,8 +310,12 @@ internal class SwapAmountModel @Inject constructor(
val state = uiState.value as? SwapAmountUM.Content ?: return
val (fromCryptoCurrency, toCryptoCurrency) = when (state.swapDirection) {
SwapDirection.Direct -> primaryCryptoCurrencyStatus.currency to secondaryCryptoCurrencyStatus.currency
SwapDirection.Reverse -> secondaryCryptoCurrencyStatus.currency to primaryCryptoCurrencyStatus.currency
SwapDirection.Direct -> {
state.primaryCryptoCurrencyStatus.currency to state.secondaryCryptoCurrencyStatus.currency
}
SwapDirection.Reverse -> {
state.secondaryCryptoCurrencyStatus.currency to state.primaryCryptoCurrencyStatus.currency
}
}
val fromAmount = when (state.swapDirection) {
@ -346,9 +335,11 @@ internal class SwapAmountModel @Inject constructor(
uiState.transformerUpdate(SwapQuoteLoadingStateTransformer)
modelScope.launch {
val quotes = swapGroups.firstOrNull {
val quotes = swapGroups.filter {
it.currencyStatus.currency.id == toCryptoCurrency.id
}?.providers?.map { provider ->
}.flatMap {
it.providers
}.map { provider ->
async {
getSwapQuoteUseCase(
userWallet = userWallet,
@ -361,7 +352,7 @@ internal class SwapAmountModel @Inject constructor(
SwapQuoteUM.Error(
provider = provider,
expressError = error,
)
).takeIf { error is ExpressError.AmountError }
},
ifRight = { quote: SwapQuoteModel ->
convertToSwapProviderUM(
@ -373,13 +364,11 @@ internal class SwapAmountModel @Inject constructor(
},
)
}
}?.awaitAll().orEmpty()
}.awaitAll().filterNotNull()
uiState.transformerUpdate(
SwapAmountSetQuotesTransformer(
quotes = quotes,
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary,
),

View file

@ -0,0 +1,28 @@
package com.tangem.features.swap.v2.impl.amount.model.converter
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
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.domain.express.models.ExpressError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.swap.v2.impl.R
import com.tangem.utils.converter.Converter
internal class SwapAmountErrorConverter(
private val cryptoCurrency: CryptoCurrency,
) : Converter<ExpressError, TextReference?> {
override fun convert(value: ExpressError): TextReference? = when (value) {
is ExpressError.AmountError.TooSmallError -> resourceReference(
R.string.express_provider_min_amount,
wrappedList(value.amount.format { crypto(cryptoCurrency = cryptoCurrency) }),
)
is ExpressError.AmountError.TooBigError -> resourceReference(
R.string.express_provider_max_amount,
wrappedList(value.amount.format { crypto(cryptoCurrency = cryptoCurrency) }),
)
else -> null
}
}

View file

@ -1,22 +1,20 @@
package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.common.ui.amountScreen.converters.AmountCurrencyTransformer
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount
import com.tangem.utils.transformer.Transformer
internal class SwapAmountChangeCurrencyTransformer(
private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val isFiatSelected: Boolean,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
return prevState.updateAmount(
onPrimaryAmount = {
copy(
amountField = AmountCurrencyTransformer(
cryptoCurrencyStatus = primaryCryptoCurrencyStatus,
cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus,
value = isFiatSelected,
).transform(prevState.primaryAmount.amountField),
)
@ -24,7 +22,7 @@ internal class SwapAmountChangeCurrencyTransformer(
onSecondaryAmount = {
copy(
amountField = AmountCurrencyTransformer(
cryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus,
value = isFiatSelected,
).transform(prevState.secondaryAmount.amountField),
)

View file

@ -3,33 +3,50 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact
import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountErrorConverter
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.utils.extensions.orZero
import com.tangem.utils.transformer.Transformer
internal class SwapAmountSelectQuoteTransformer(
private val quoteUM: SwapQuoteUM,
private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryMaximumAmountBoundary: EnterAmountBoundary,
private val secondaryMinimumAmountBoundary: EnterAmountBoundary,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
val providerErrorConverter = SwapAmountErrorConverter(
cryptoCurrency = prevState.primaryCryptoCurrencyStatus.currency,
)
return prevState.copy(
isPrimaryButtonEnabled = true,
isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content,
selectedQuote = quoteUM,
secondaryAmount = if (
prevState.selectedAmountType == SwapAmountType.From && prevState.swapDirection == SwapDirection.Direct
) {
primaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) {
val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content
val amountField = swapAmountField?.amountField as? AmountState.Data
val amountError = (quoteUM as? SwapQuoteUM.Error)?.expressError?.let(providerErrorConverter::convert)
swapAmountField?.copy(
amountField = amountField?.copy(
amountTextField = amountField.amountTextField.copy(
error = amountError ?: TextReference.EMPTY,
isError = amountError != null,
),
) ?: swapAmountField.amountField,
) ?: prevState.primaryAmount
} else {
prevState.primaryAmount
},
secondaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) {
val secondaryAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content
val fromAmount = (prevState.primaryAmount.amountField as? AmountState.Data)
?.amountTextField?.cryptoAmount?.value.orZero()
@ -38,17 +55,17 @@ internal class SwapAmountSelectQuoteTransformer(
swapDirection = prevState.swapDirection,
fromTokenAmount = fromAmount,
toTokenAmount = toAmount.orZero(),
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus,
)
secondaryAmountField?.copy(
priceImpact = priceImpact,
amountField = AmountFieldChangeTransformer(
cryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus,
maxEnterAmount = secondaryMaximumAmountBoundary,
minimumTransactionAmount = secondaryMinimumAmountBoundary,
value = toAmount?.parseBigDecimal(secondaryCryptoCurrencyStatus.currency.decimals)
value = toAmount?.parseBigDecimal(prevState.secondaryCryptoCurrencyStatus.currency.decimals)
.orEmpty(),
).transform(secondaryAmountField.amountField),
) ?: prevState.secondaryAmount

View file

@ -1,29 +1,15 @@
package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.core.ui.extensions.TextReference
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.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.swap.v2.impl.R
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent
import com.tangem.utils.StringsSigns
import com.tangem.utils.extensions.isPositive
import com.tangem.utils.extensions.orZero
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@ -31,91 +17,30 @@ import java.math.BigDecimal
internal class SwapAmountSetQuotesTransformer(
private val quotes: List<SwapQuoteUM>,
private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryMaximumAmountBoundary: EnterAmountBoundary,
private val secondaryMinimumAmountBoundary: EnterAmountBoundary,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
val fromAmount = when (prevState.swapDirection) {
SwapDirection.Direct -> prevState.primaryAmount.amountField
SwapDirection.Reverse -> prevState.secondaryAmount.amountField
} as? AmountState.Data
val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value.orZero()
val sortedQuotes = quotes.sortedWith(SwapQuotesComparator)
val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty
return prevState.copy(
isPrimaryButtonEnabled = bestQuote is SwapQuoteUM.Content,
val selectQuoteTransformer = SwapAmountSelectQuoteTransformer(
quoteUM = bestQuote,
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary,
)
val updatedState = selectQuoteTransformer.transform(prevState = prevState)
if (updatedState !is SwapAmountUM.Content) return prevState
return updatedState.copy(
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled && quotes.isNotEmpty(),
swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote),
selectedQuote = bestQuote,
primaryAmount = if (
prevState.selectedAmountType == SwapAmountType.From && prevState.swapDirection == SwapDirection.Direct
) {
val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content
val amountField = swapAmountField?.amountField as? AmountState.Data
val amountError = (bestQuote as? SwapQuoteUM.Error)?.expressError.getAmountError()
if (amountField?.amountTextField?.isError == true) {
prevState.primaryAmount
} else {
swapAmountField?.copy(
amountField = amountField?.copy(
amountTextField = amountField.amountTextField.copy(
error = amountError ?: TextReference.EMPTY,
isError = amountError != null,
),
) ?: swapAmountField.amountField,
) ?: prevState.primaryAmount
}
} else {
prevState.primaryAmount
},
secondaryAmount = if (
prevState.selectedAmountType == SwapAmountType.From && prevState.swapDirection == SwapDirection.Direct
) {
val amountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content
val toAmount = (bestQuote as? SwapQuoteUM.Content)?.quoteAmount
val priceImpact = calculatePriceImpact(
swapDirection = prevState.swapDirection,
fromTokenAmount = fromAmountValue,
toTokenAmount = toAmount.orZero(),
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
)
amountField?.copy(
priceImpact = priceImpact,
amountField = AmountFieldChangeTransformer(
cryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
maxEnterAmount = secondaryMaximumAmountBoundary,
minimumTransactionAmount = secondaryMinimumAmountBoundary,
value = toAmount?.parseBigDecimal(secondaryCryptoCurrencyStatus.currency.decimals).orEmpty(),
).transform(amountField.amountField),
) ?: prevState.secondaryAmount
} else {
prevState.secondaryAmount
},
)
}
private fun ExpressError?.getAmountError(): TextReference? = when (this) {
is ExpressError.AmountError.TooSmallError -> resourceReference(
R.string.express_provider_min_amount,
wrappedList(amount.format { crypto(cryptoCurrency = primaryCryptoCurrencyStatus.currency) }),
)
is ExpressError.AmountError.TooBigError -> resourceReference(
R.string.express_provider_max_amount,
wrappedList(amount.format { crypto(cryptoCurrency = primaryCryptoCurrencyStatus.currency) }),
)
else -> null
}
private fun getQuotesWithDiff(sortedQuotes: List<SwapQuoteUM>, bestQuote: SwapQuoteUM): ImmutableList<SwapQuoteUM> {
return sortedQuotes.sortedWith(SwapQuotesComparator)
.map { quote ->

View file

@ -2,15 +2,11 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount
import com.tangem.utils.transformer.Transformer
@Suppress("LongParameterList")
internal class SwapAmountValueChangeTransformer(
private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val primaryMaximumAmountBoundary: EnterAmountBoundary,
private val secondaryMaximumAmountBoundary: EnterAmountBoundary,
private val primaryMinimumAmountBoundary: EnterAmountBoundary,
@ -25,7 +21,7 @@ internal class SwapAmountValueChangeTransformer(
onPrimaryAmount = {
copy(
amountField = AmountFieldChangeTransformer(
cryptoCurrencyStatus = primaryCryptoCurrencyStatus,
cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus,
maxEnterAmount = primaryMaximumAmountBoundary,
minimumTransactionAmount = primaryMinimumAmountBoundary,
value = value,
@ -35,7 +31,7 @@ internal class SwapAmountValueChangeTransformer(
onSecondaryAmount = {
copy(
amountField = AmountFieldChangeTransformer(
cryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus,
maxEnterAmount = secondaryMaximumAmountBoundary,
minimumTransactionAmount = secondaryMinimumAmountBoundary,
value = value,

View file

@ -2,15 +2,12 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.utils.transformer.Transformer
internal class SwapAmountValueMaxTransformer(
private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val primaryMaximumAmountBoundary: EnterAmountBoundary,
private val secondaryMaximumAmountBoundary: EnterAmountBoundary,
private val primaryMinimumAmountBoundary: EnterAmountBoundary,
@ -25,7 +22,7 @@ internal class SwapAmountValueMaxTransformer(
onPrimaryAmount = {
copy(
amountField = AmountFieldSetMaxAmountTransformer(
cryptoCurrencyStatus = primaryCryptoCurrencyStatus,
cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus,
maxAmount = primaryMaximumAmountBoundary,
minAmount = primaryMinimumAmountBoundary,
).transform(prevState.primaryAmount.amountField),
@ -34,7 +31,7 @@ internal class SwapAmountValueMaxTransformer(
onSecondaryAmount = {
copy(
amountField = AmountFieldSetMaxAmountTransformer(
cryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus,
maxAmount = secondaryMaximumAmountBoundary,
minAmount = secondaryMinimumAmountBoundary,
).transform(prevState.secondaryAmount.amountField),

View file

@ -0,0 +1,201 @@
package com.tangem.features.swap.v2.impl.amount.ui
import android.content.res.Configuration
import androidx.compose.foundation.Canvas
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.RoundedCornerShape
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.geometry.Offset
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.StrokeCap
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.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.ui.AmountBlockV2
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.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.swap.v2.impl.R
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview
import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent
@Suppress("DestructuringDeclarationWithTooManyEntries")
@Composable
internal fun SwapAmountBlockContent(
amountUM: SwapAmountUM,
isClickEnabled: Boolean,
onProviderSelectClick: () -> Unit,
onInfoClick: () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (amountUM !is SwapAmountUM.Content) return
ConstraintLayout(
modifier = modifier
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.action)
.clickable(
indication = ripple(),
interactionSource = remember { MutableInteractionSource() },
enabled = isClickEnabled,
onClick = onClick,
),
) {
val (from, to, separator, provider) = createRefs()
AmountBlockV2(
amountState = amountUM.primaryAmount.amountField,
isClickDisabled = true,
isEditingDisabled = false,
modifier = Modifier.constrainAs(from) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
extraContent = {
SwapPriceImpact(amountFieldUM = amountUM.primaryAmount, onInfoClick = onInfoClick)
},
)
AmountBlockV2(
amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy(
title = resourceReference(R.string.send_with_swap_recipient_amount_title),
availableBalance = TextReference.EMPTY,
) ?: amountUM.secondaryAmount.amountField,
isClickDisabled = true,
isEditingDisabled = false,
modifier = Modifier.constrainAs(to) {
top.linkTo(from.bottom, 8.dp)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
extraContent = {
SwapPriceImpact(amountFieldUM = amountUM.secondaryAmount, onInfoClick = onInfoClick)
},
)
SwapAmountDivider(
modifier = Modifier.constrainAs(separator) {
top.linkTo(from.bottom)
bottom.linkTo(to.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
)
SwapChooseProviderContent(
expressProvider = amountUM.selectedQuote.provider,
onClick = onProviderSelectClick,
modifier = Modifier.constrainAs(provider) {
top.linkTo(to.bottom)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
)
}
}
@Composable
private fun SwapPriceImpact(amountFieldUM: SwapAmountFieldUM, onInfoClick: () -> Unit) {
val priceImpact = (amountFieldUM as? SwapAmountFieldUM.Content)?.priceImpact
if (priceImpact != null) {
Text(
text = priceImpact.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.attention,
)
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_information_24),
),
tint = TangemTheme.colors.icon.attention,
contentDescription = null,
modifier = Modifier
.size(20.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onClick = onInfoClick,
),
)
}
}
@Composable
private fun SwapAmountDivider(modifier: Modifier = Modifier) {
Box(modifier = modifier) {
SwapDivider()
Text(
text = stringResourceSafe(R.string.swap_via_provider),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.background(TangemTheme.colors.stroke.primary, RoundedCornerShape(32.dp))
.padding(1.dp)
.background(TangemTheme.colors.text.primary2, RoundedCornerShape(32.dp)) // workaround
.background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f), RoundedCornerShape(32.dp))
.padding(horizontal = 11.dp, vertical = 5.dp)
.align(Alignment.Center),
)
}
}
@Suppress("MagicNumber")
@Composable
private fun BoxScope.SwapDivider() {
val color = TangemTheme.colors.background.tertiary
Canvas(
Modifier
.fillMaxWidth()
.align(Alignment.Center),
) {
val gapWidth = 2.dp.toPx()
val radius = 2.dp.toPx()
val pathEffect = PathEffect.dashPathEffect(
intervals = floatArrayOf(gapWidth, gapWidth * 3),
phase = 0f,
)
drawLine(
color = color,
start = Offset(0f, 0f),
end = Offset(size.width, 0f),
pathEffect = pathEffect,
cap = StrokeCap.Round,
strokeWidth = radius,
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun SwapAmountBlockContent_Preview() {
TangemThemePreview {
SwapAmountBlockContent(
amountUM = SwapAmountContentPreview.defaultState,
isClickEnabled = true,
onProviderSelectClick = {},
onInfoClick = {},
onClick = {},
)
}
}
// endregion

View file

@ -6,6 +6,8 @@ import com.tangem.features.swap.v2.impl.amount.model.SwapAmountClickIntents
internal object SwapAmountClickIntentsStub : SwapAmountClickIntents {
override fun onExpandEditField(selectedAmountType: SwapAmountType) {}
override fun onInfoClick() {}
override fun onSelectTokenClick() {}
override fun onAmountValueChange(value: String) {}

View file

@ -6,8 +6,11 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
@ -16,6 +19,34 @@ import kotlinx.collections.immutable.persistentListOf
internal data object SwapAmountContentPreview {
private val cryptoCurrencyStatus = CryptoCurrencyStatus(
currency = CryptoCurrency.Coin(
id = CryptoCurrency.ID.fromValue("coin⟨BITCOIN⟩bitcoin"),
network = Network(
id = Network.ID(
value = "bitcoin",
derivationPath = Network.DerivationPath.None,
),
backendId = "bitcoin",
name = "Bitcoin",
currencySymbol = "BTC",
derivationPath = Network.DerivationPath.None,
isTestnet = false,
standardType = Network.StandardType.Unspecified("bitcoin"),
hasFiatFeeRate = false,
canHandleTokens = false,
transactionExtrasType = Network.TransactionExtrasType.NONE,
),
name = "Bitcoin",
symbol = "BTC",
decimals = 8,
iconUrl = "",
isCustom = false,
),
value = CryptoCurrencyStatus.Loading,
)
val emptyState = SwapAmountUM.Content(
isPrimaryButtonEnabled = false,
primaryAmount = SwapAmountFieldUM.Empty(
@ -29,8 +60,8 @@ internal data object SwapAmountContentPreview {
swapCurrencies = SwapCurrencies.EMPTY,
swapQuotes = persistentListOf(),
selectedQuote = SwapQuoteUM.Empty,
primaryCryptoCurrencyStatus = null,
secondaryCryptoCurrencyStatus = null,
primaryCryptoCurrencyStatus = cryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = cryptoCurrencyStatus,
swapRateType = ExpressRateType.Float,
appCurrency = AppCurrency.Default,
)
@ -65,8 +96,8 @@ internal data object SwapAmountContentPreview {
swapCurrencies = SwapCurrencies.EMPTY,
swapQuotes = persistentListOf(),
selectedQuote = SwapQuoteUM.Empty,
primaryCryptoCurrencyStatus = null,
secondaryCryptoCurrencyStatus = null,
primaryCryptoCurrencyStatus = cryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = cryptoCurrencyStatus,
swapRateType = ExpressRateType.Float,
isPrimaryButtonEnabled = true,
)