diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt index 611e613b72..b9fa864b84 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt @@ -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 = {}, diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index e90c8e89e2..f369cd7218 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -574,13 +574,13 @@ Netzwerk nicht hinzugefügt Nicht hinzugefügt Nicht unterstützte NFT-Arten - cNFTs und pNFTs werden noch nicht unterstützt + cNFTs und pNFTs werden derzeit nicht unterstützt. Bitte sende diese nicht an Deine Wallet. NFT senden Eigenschaften Unbetitelte Sammlung %1$d NFTs in der %2$d Sammlung - + %1$d NFT in der %2$d Sammlung %1$d NFTs in der %2$d Sammlung Tippe hier, um das erste NFT zu erhalten @@ -821,6 +821,7 @@ Überprüfe deine Netzwerkverbindung Informationen zur Netzwerkgebühr nicht erreichbar Von + Von %s Grenzwert Gasgebühr 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. Gaspreis @@ -828,6 +829,7 @@ Max Höchstbetrag Gebühr bis zu + Memo: %s Ungültiges Memo Abdeckung der Netzgebühren Nonce @@ -878,6 +880,7 @@ Die Netzwerkgebühr wird durch die Nutzung von %1$s Energieträgern gedeckt. Die Netzwerkgebühr wird durch den Verbrauch von %1$s Energie reduziert inklusive einer Netzgebühr von %1$s + Zur Adresse Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert %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. Der Betrag geht über die Bilanz hinaus @@ -887,6 +890,7 @@ Die Gebühr geht über die Bilanz hinaus Der Gesamtbetrag geht über die Bilanz hinaus Sende uns ein Token, und wir konvertieren es unterwegs. Dein Empfänger erhält genau das, was er braucht – nahtlos. + Wird an den Empfänger gesendet Senden mit Swap Transaktion gesendet Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest. @@ -1316,7 +1320,7 @@ Wir haben einen unbekannten Fehler festgestellt. Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht. Nicht unterstützte Netzwerke - Tangem unterstützt ein erforderliches Netzwerk um %s. + Tangem unterstützt ein erforderliches Netzwerk um %s Verifizierte Domain Falsche Karte oder falscher Ring in der App ausgewählt Wir haben eine Art Problem diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index bc9fd8a0e8..46aea2ae3b 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -560,7 +560,7 @@ Colección sin título %1$d NFT en %2$d colecciones - + %1$d NFT en %2$d coleccione %1$d NFTs en %2$d colecciones Pulse aquí para recibir el primer NFT diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 5bd4e39551..b3b307cac4 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -561,7 +561,7 @@ Collection sans titre %1$d NFT dans la collection %2$d - + %1$d NFT dans la collection %2$d %1$d NFT dans la collection %2$d Appuyez ici pour recevoir le premier NFT diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 11cb1fb068..731aa8038e 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1,5 +1,14 @@ + アクセスコードがないとウォレットは保護されません。 + とにかくスキップ + アクセスコードが設定されていません + アクセスコードを入力 + 続行するには、以前に入力したコードを確認してください + アクセスコードを再入力 + ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 + アクセスコードの作成 + アクセスコード トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して買付できるようにします。 トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して売却できるようにします。 売却 @@ -49,6 +58,17 @@ システムのデフォルト テーマ アプリ設定 + ウォレットのバックアップが正常に完了しました。 + バックアップが完了しました + シークレットリカバリーフレーズは、ウォレットへのアクセスと復元のために使用される%sのランダムな単語のセットです。 + これらの単語は紛失した場合、復元できません。必ず安全な場所に保管してください。 + 安全に保管してください + これらの%s個の単語をパスワードマネージャーなどの安全な場所に保存し、決して他の人と共有しないでください。 + 復元は不可能です + リカバリーフレーズ + これらの%s語を順番に書き留めて、安全かつプライベートに保管してください。 + ウォレットと、リカバリーフレーズのセキュリティとバックアップの全責任は、Tangemではなくユーザーにあります。 + リカバリーフレーズ 残高を表示または非表示にするには、デバイスの画面を下向きにするか、設定でオフにしてください。 今後表示しない わかりました @@ -101,6 +121,7 @@ 承認 承認 注意 + バックアップ 残高: %s 残高 生体認証 @@ -117,10 +138,12 @@ 請求 報酬を受け取る 閉じる + 近日公開 確認 Tangemサポートへ問い合わせる Visaサポートへ問い合わせる 続ける + 変換する コピー アドレスをコピー 作成 @@ -150,6 +173,7 @@ 遅い 速度と料金 終了 + 無料 アドレスを同期する プロバイダーへ移動 トークンへ移動 @@ -202,6 +226,7 @@ もっと見る 署名 署名して送信 + スキップ ステーキング ステーキング 始める @@ -222,6 +247,7 @@ アクセスできません ステーキング解除 %1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。 + 値がコピーされました はい @@ -377,10 +403,24 @@ 許可を与える 無制限 既存のウォレットを追加 + 新しいウォレットを作成する Tangemを注文 Tangemをスキャン %sへ %sネットワーク + iCloudバックアップに保存されている既存のウォレットを復元する + iCloudバックアップ + バックアップへ移動 + アクセスコードを使用してウォレットを保護するには、まずバックアップを完了してください。 + まずバックアップを完了する + バックアップなし + 暗号資産をオフラインで安全に保管する物理カード + リカバリーフレーズ + 受信取引の通知を受け取る + 鍵はアプリに保存されます + 最新の機能とニュースをお届けします + シードフレーズのバックアップ + モバイルウォレットを作成する この情報はAIで生成されました。 \nエラーが見つかった場合は、ここをタップしてください。 アクセスコードを変更するには、上図のようにカードまたはリングをタップし、操作が終了するまで取り外さないでください。 パスコードを変更するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。 @@ -565,7 +605,7 @@ ネットワークが追加されていません 未追加 サポートされていないNFTタイプ - cNFTとpNFTはまだサポートされていません + cNFTとpNFTはまだサポートされていません。ウォレットに送信しないでください。 NFTを送信する 特徴 無題のコレクション @@ -621,6 +661,7 @@ 秘密鍵を非公開で生成する カードは有効化され、使用可能になりました 成功! + ウォレットの設定が完了し、使用できるようになりました。 この場合は、最初からやり直す必要があります。 アクティベーション処理を途中で終了しますか? スタート @@ -783,7 +824,9 @@ 推奨手数料を下回る手数料が指定されたため、取引に遅れが生じる可能性があります。続行しますか? 理由: %1$s \nコード: %2$s 取引は完了していません + 別のトークンに変換する 金額 + 受取人に送信されます 取引手数料は、vByteフィールドのSatoshiの値を調整して設定できます。 取引にかかる手数料です。自由に設定できます。 最大手数料 @@ -807,6 +850,7 @@ ネットワーク接続を確認してください ネットワーク手数料についての情報にアクセスできません より + %sから ガス上限 これは、取引または契約を完了するために使用されるガスの最大額です。ガス上限を設定することで、取引実行時に予期せぬ請求や無制限の請求を防ぐことができます。 ガス代 @@ -814,6 +858,7 @@ 最大 最大金額 最大手数料 + メモ: %s 無効なメモ ネットワーク手数料のカバー ナンス @@ -827,6 +872,7 @@ カスタム手数料が高くなっています %1$sネットワークの特殊性により、残高全体を転送する場合の手数料は高くなります。手数料を削減するには、 %2$sを残します。 手数料が高くなっています + 受取人の口座が有効化されていません。最低送金額はレント免除残高%1$s以上である必要があります。 口座残高はレンタル料金より低くすることはできません。残高を少なくとも%1$sに維持するか、すべての資金を引き出してください。 手数料が送金金額を超えており、マイナスの値になってしまいます。 無効な金額 @@ -845,7 +891,7 @@ 最近の取引 受取人 有効なアドレスではありません - トークンを失わないように、受信ウォレットアドレスが%sネットワーク上にあることを確認してください。 + トークンの損失を避けるために、受信ウォレットアドレスが** %sネットワーク**上にあることを確認してください %s に送信 %sネットワークアドレスを確認してください。エラーが発生すると転送が失われる可能性があります。 送金先 @@ -864,6 +910,7 @@ ネットワーク手数料は%1$sエネルギーを使用してカバーされます %1$sエネルギーを使用するとネットワーク手数料が減額されます ネットワーク手数料%1$sを含む + アドレスへ 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 %1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 金額が残高を超えています @@ -873,6 +920,8 @@ 手数料が残高を超えています 合計金額が残高を超えています トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。 + 受信者に送信されます + 受取金額 スワップして送信 取引が送信されました 設定したいカードまたはリングをスキャンするために準備してください。 @@ -1028,6 +1077,7 @@ 破格のレート 手間がかからず直感的に操作でき、数回タップするだけでトークンを交換できます。 とにかく便利 + プロバイダー経由のスワップ この金額には以下が含まれます:\n- サービスプロバイダーの手数料\n- 取引所からユーザーのアドレスに%s を送り返すためのネットワーク手数料。 金額には以下が含まれます: \n • サービス プロバイダーの手数料\n • 取引所からユーザーのアドレスに%1$sを送金するためのネットワーク手数料。 \n\nプロバイダーのスリッページは最大%2$sです この金額には、サービスプロバイダーの手数料が含まれています。 @@ -1178,6 +1228,29 @@ dAppsに接続する WalletConnect 接続には数秒かかる場合があります + %sから + Tangemウォレットを購入 — 暗号資産をオフラインで安全に保管できる物理カードです。 + ハードウェアウォレット + 数秒でスマートフォンに安全なウォレットが作成されます。 + モバイルウォレット + 何を選ぶべき? + すでにTangemウォレットをお持ちですか? + 今すぐスキャン + ウォレットの作成方法を選択してください + Tangemウォレットを購入しますか? + 今すぐ購入 + Googleドライブのバックアップに保存されている既存のウォレットを復元する + Googleドライブからインポート + 既存のウォレットを追加 + 暗号資産をオフラインで安全に保管する物理カード。 + Tangemウォレットをスキャンする + ウォレットを復元するためのシードフレーズ + ウォレットをインポート + リカバリーフレーズをインポートする + ウォレットのバックアップが正常に完了しました。 + ウォレットをインポート + インポート完了 + ウォレットをインポート %s市場価格 直近24時間 %sネットワーク @@ -1186,6 +1259,8 @@ 今すぐ10 % オフで購入 1.3万種類以上の暗号資産にアクセス。ワンタップで買付、売却、スワップ、ステーキングが可能です。\nバックアップ用に最大3枚のカードを連携できます。 Tangemウォレットを見る + このウォレットを保護するためのシークレットコードです。ログインと署名に使用されます。 + アクセスコードの設定 / 変更 ウォレットの受信取引とTangemの更新について通知を受け取る。 取引通知 ウォレット設定 @@ -1274,6 +1349,10 @@ 下にスワイプして更新するか、後でもう一度お試しください。 一部のネットワークにアクセスできません 一部のトークン残高を更新できませんでした + トラストラインを有効にする + %sが足りません。このトークンを関連付けるには、XLMアカウントに資金を追加してください。 + このトークンを受け取るには、トラストラインを有効にする必要があります。ネットワークには 0.5 XLMの準備金が必要です。 + トラストラインが必要 これはテストネットカードです。取引処理はできませんので、テストおよび開発目的でのみご利用ください。 テスト目的のみ 残高が古い可能性があります。ページを更新してください。 @@ -1296,7 +1375,7 @@ 不明なエラーが発生しました Tangemは現在%sで必要なネットワークをサポートしていません。 サポートされていないネットワーク - Tangemは%sで必要なネットワークをサポートします。 + Tangemは%sで必要なネットワークをサポートします 検証済みドメイン アプリで間違ったカードまたはリングが選択されました 問題が起きています diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 8ac257b580..4c4ce1336a 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -223,6 +223,7 @@ Недоступно Завершить стейкинг Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. + Значение скопировано неделю с Да @@ -843,6 +844,7 @@ Установлена высокая комиссия Ввиду особенности сети %1$s комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %2$s. Комиссия повышена + Аккаунт получателя не активирован. Минимальная сумма перевода должна быть не меньше баланса, необходимого для покрытия арендной платы: %1$s. Баланс вашего счета не может быть ниже арендной платы. Пожалуйста, оставьте на счете не менее %1$s или выведите все средства. Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению Недопустимая сумма @@ -1259,6 +1261,9 @@ Свайпните вниз для обновления или попробуйте позже. Некоторые сети недоступны Некоторые балансы токенов не удалось обновить + Открыть Trustline + Чтобы получить этот токен, необходимо открыть Trustline. Сеть удержит резерв в размере 0.5 XLM. + Откройте Trustline Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки. Только для целей тестирования Возможно, баланс устарел. Обновите страницу. diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 314b79ecef..d1048dabe7 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -574,7 +574,7 @@ Риси NFTs в %2$d колекціях%1$d - + %1$d NFT в %2$d колекціi %1$d NFTs в %2$d колекціях diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 28efd231b8..9499629f9e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1,5 +1,14 @@ + Your wallet won’t be protected without an Access Code. + Skip anyway + Access Code not set + Enter Access Code + Confirm your previously entered code to continue + Re-enter Access Code + Set a %s-digit Access Code to unlock your wallet. + Create Access Code + Access code Can’t find your token? Go to the Market section on the main page and add it to your portfolio for purchase Can’t find your token? Go to the Market section on the main page and add it to your portfolio for selling. Sell @@ -49,6 +58,17 @@ System default Theme App settings + You successfully backed up your wallet. + Backup Completed + Your Secret Recovery Phrase is a fixed set of %s random words used to access and recover your wallet. + These words can’t be recovered if lost. Make sure to keep it somewhere secure. + Keep It Safe + Save these %s words in a secure location, such as a password manager, and never share them with anyone. + No Recovery Possible + Recovery phrase + Write down these %s words in order and keep them safe and private + Full responsibility for the security and backup of the wallet and recovery phrase lies with the user, not with Tangem. + Recovery phrase To hide or show your balances, simply flip your device screen down, or switch it off in Settings Don\'t show again Got it @@ -103,6 +123,7 @@ Approval Approve Attention + Backup Balance: %s Balance biometric authentication @@ -124,6 +145,7 @@ Contact Tangem Support Contact Visa Support Continue + Convert Copy Copy address Create @@ -208,6 +230,7 @@ Show more Sign Sign and send + Skip Stake Staking Start @@ -229,6 +252,7 @@ Unreachable Unstake 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. + Value copied week with Yes @@ -596,7 +620,7 @@ Untitled collection %1$d NFTs in %2$d collections - + %1$d NFT in %2$d collection %1$d NFTs in %2$d collections Tap here to receive first NFT @@ -647,6 +671,7 @@ Generate keys privately Your card is activated and ready to be used Success! + Your wallet is set up and ready to use! In this case, you will need to start from the beginning. Do you want to exit the activation process? Getting started @@ -813,7 +838,9 @@ You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue? Reason: %1$s\nCode: %2$s The transaction is not completed + Convert to another token Amount + Will be sent to recipient You can set your transaction fee by adjusting the value in the Satoshi per vByte field. The fee that will be charged for your transaction. You can set your own value. Max fee @@ -859,6 +886,7 @@ Custom fee is high 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. The fee is higher + The recipient account is not activated. The minimum transfer amount must be equal to or greater than the rent-exempt balance: %1$s. Your account balance cannot be lower than the rent fee. Please maintain at least %1$s on your account or withdraw all funds. The included commission exceeds the transfer amount, leading to a negative value Invalid amount @@ -877,7 +905,7 @@ Recent Recipient Not a valid address - Ensure the receiving wallet address is on the %s network to avoid losing your tokens + Ensure the receiving wallet address is on **the %s network** to avoid losing your tokens send to %s Ensure you %s network address, as errors may result in lost transfers Send to @@ -907,6 +935,7 @@ Total amount exceeds balance Send any token, and we’ll convert it on the way. Your recipient gets exactly what they need—seamlessly. Will be sent a recipient + Amount to receive Send with swap Transaction sent Prepare to scan card or ring you want to set up. @@ -1062,6 +1091,7 @@ Unbeatable Rates Hassle-free and intuitive, allowing you to swap tokens in just a few taps Simply Convenient + Swap via provider The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address. 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 The amount includes the service provider\'s fee. @@ -1292,6 +1322,8 @@ Get now with 10% off Access 13,000+ cryptocurrencies. Buy, sell, swap, and stake with a single tap.\nLink up to three cards for a backup. Discover Tangem Wallet + Secret code to protect this wallet. Used for login and signatures. + Set/Change Access Code Stay notified on wallet incoming transactions and Tangem updates. Transaction Notifications Wallet settings @@ -1381,6 +1413,10 @@ Swipe down to refresh or try again later. Some networks are unreachable Some token balances could not be updated + Enable Trustline + Not enough %s. Top up your XLM account to associate this token + A Trustline must be enabled to receive this token. The network requires a 0.5 XLM reserve. + Trustline Required This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes. For testing purposes only Balance may be outdated. Refresh the page. @@ -1403,7 +1439,7 @@ We\'ve encountered unknown error Tangem does not currently support a required network by %s. Unsuported networks - Tangem support a required network by %s. + Tangem support a required network by %s Verified domain Wrong card or ring selected in the App We\'ve got some kind of problem diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt index 7bf133c215..fe6e07f89a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt @@ -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, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt new file mode 100644 index 0000000000..f0f3fc46a4 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt @@ -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, + val cryptoCurrency: CryptoCurrency, + val selectedProvider: ExpressProvider, + ) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt index b482191a87..beb46d4941 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt @@ -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 + abstract val primaryCryptoCurrencyStatusFlow: StateFlow + abstract val secondaryCryptoCurrency: CryptoCurrency? data class AmountParams( override val amountUM: SwapAmountUM, override val analyticsCategoryName: String, override val userWallet: UserWallet, override val isBalanceHidingFlow: StateFlow, - override val appCurrency: AppCurrency, override val swapDirection: SwapDirection, - val primaryCryptoCurrency: CryptoCurrency, - val secondaryCryptoCurrency: CryptoCurrency?, + override val primaryCryptoCurrencyStatusFlow: StateFlow, + override val secondaryCryptoCurrency: CryptoCurrency?, val callback: SwapAmountComponent.ModelCallback, val currentRoute: Flow, ) : SwapAmountComponentParams() @@ -37,10 +36,9 @@ internal sealed class SwapAmountComponentParams { override val analyticsCategoryName: String, override val userWallet: UserWallet, override val isBalanceHidingFlow: StateFlow, - override val appCurrency: AppCurrency, override val swapDirection: SwapDirection, - val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, - val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?, + override val primaryCryptoCurrencyStatusFlow: StateFlow, + override val secondaryCryptoCurrency: CryptoCurrency?, val blockClickEnableFlow: StateFlow, ) : SwapAmountComponentParams() } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index 62b7c86556..2ba34fa932 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -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, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAlertFactory.kt new file mode 100644 index 0000000000..80af642d28 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAlertFactory.kt @@ -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() }, + ), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt index f9c92b77a1..34cf98f664 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt @@ -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() } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index a7610c26f1..97287393a0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -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 = SlotNavigation() + val uiState: StateFlow 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, ), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountErrorConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountErrorConverter.kt new file mode 100644 index 0000000000..36b944a04d --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountErrorConverter.kt @@ -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 { + + 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 + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt index 05c0a9d16b..e1a149555d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt @@ -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 { 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), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index 0c11067bfc..b11d0238e1 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -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 { 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 diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index ac39b1417e..d890417658 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -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, - private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, - private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, private val secondaryMaximumAmountBoundary: EnterAmountBoundary, private val secondaryMinimumAmountBoundary: EnterAmountBoundary, ) : Transformer { 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, bestQuote: SwapQuoteUM): ImmutableList { return sortedQuotes.sortedWith(SwapQuotesComparator) .map { quote -> diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt index 59687934ce..1c0dbd8086 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt @@ -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, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt index 8392215305..980216e2a3 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt @@ -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), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt new file mode 100644 index 0000000000..291627de98 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -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 \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt index 98bcb53665..194e61c3d3 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt @@ -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) {} diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index 6db113e583..596cecef01 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -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, )