From 90c43018fd612e93d82a9a20ca80177f1d56268a Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 4 Jan 2026 17:20:58 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../com/tangem/common/TangemBlogUrlBuilder.kt | 4 + core/res/src/main/res/values-de/strings.xml | 18 +- core/res/src/main/res/values-es/strings.xml | 2 + core/res/src/main/res/values-ja/strings.xml | 45 +- core/res/src/main/res/values-ru/strings.xml | 20 + .../src/main/res/values-uk-rUA/strings.xml | 446 +++++++++++++++ core/res/src/main/res/values/strings.xml | 4 + features/send-v2/impl/build.gradle.kts | 1 + .../DefaultFeeSelectorBlockComponent.kt | 4 +- .../DefaultFeeSelectorComponent.kt | 55 +- .../component/FeeSelectorComponentParams.kt | 10 + .../speed/FeeSpeedSelectorComponent.kt | 44 ++ .../speed/FeeSpeedSelectorIntents.kt | 15 + .../speed/model/FeeSpeedSelectorModel.kt | 39 ++ .../speed/ui/FeeSpeedSelectorContent.kt | 513 ++++++++++++++++++ .../feeselector/di/FeeSelectorModelModule.kt | 12 + .../model/FeeSelectorBlockModel.kt | 78 +++ .../feeselector/model/FeeSelectorIntents.kt | 2 + .../v2/feeselector/model/FeeSelectorLogic.kt | 202 +++++++ .../v2/feeselector/model/FeeSelectorModel.kt | 239 +------- .../v2/feeselector/route/FeeSelectorRoute.kt | 30 + .../ui/FeeSelectorModalBottomSheet.kt | 500 ++--------------- .../PreviewFeeSelectorBlockComponent.kt | 9 + 23 files changed, 1590 insertions(+), 702 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/FeeSelectorComponentParams.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/FeeSpeedSelectorComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/FeeSpeedSelectorIntents.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/model/FeeSpeedSelectorModel.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/ui/FeeSpeedSelectorContent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorBlockModel.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/route/FeeSelectorRoute.kt diff --git a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt index 0319e1c59f..1ba2efe988 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt @@ -31,5 +31,9 @@ object TangemBlogUrlBuilder { data object WhatWalletToChoose : Post { override val path: String = "mobile-wallet" } + + data object WhatIsTransactionFee : Post { + override val path: String = "what-is-a-transaction-fee-and-why-do-we-need-it" + } } } \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 81ee6497c5..0b971535c9 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -61,8 +61,8 @@ Bist Du sicher, dass Du das neue Konto verwerfen willst? Bist Du sicher, dass Du die Bearbeitungen verwerfen willst? Nicht gespeicherte Änderungen - Einige benutzerdefinierte Token wurden von “%1$s” Zu “%2$s” da ihre Herleitung zu diesem Konto gehört. - Einige benutzerdefinierte Token wurden verschoben. + Einige benutzerdefinierte Token werden automatisch von „%1$s\" Zu \"%2$sda ihre Herleitung zu diesem Konto gehört. + Einige benutzerdefinierte Token werden automatisch verschoben. Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum kaufen im Portfolio hinzu. Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum verkaufen im Portfolio hinzu. Verkaufen @@ -182,8 +182,8 @@ Alle Tangem-Geräte wurden zurückgesetzt. Du kannst nun mit dem Upgrade Deiner Wallet fortfahren. Erneut aktualisieren Zurücksetzen abgeschlossen - Du hast nicht all Deine Tangem-Geräte zurückgesetzt. - Du hast nicht alle Deine Tangem-Geräte zurückgesetzt. + Wir empfehlen, den Reset-Prozess für alle Tangem-Geräte in dieser Wallet abzuschließen. + Einige Tangem-Geräte müssen noch zurückgesetzt werden. Deaktiviere diese Option, wenn du nicht möchtest, dass diese Karte oder Ring zum Zurücksetzen von Zugangscodes auf anderen Karten oder Ringe in dieser Wallets verwendet wird. Bitte beachte, dass du dann auch den Zugangscode auf dieser Karte oder Ring nicht zurücksetzen kannst. Ermöglicht die Verwendung dieser Karte oder Ring zum Zurücksetzen des Zugangscodes auf anderen Karten oder Ringe in dieser Brieftasche Zugangspin resetten @@ -276,7 +276,7 @@ Aktivieren Aktiviert Fehler - Geschätzte Gebühr + Aufladegebühr Umtausch Erkunden Transaktionsverlauf einsehen @@ -539,6 +539,10 @@ ID: %s Transaktions-ID kopiert Tauschen jeden Vermögenswert in Deinem Portfolio gegen diesen Token + Eine höhere Geschwindigkeit bedeutet eine schnellere Bestätigung und eine höhere Netzgebühr. %s + Geschwindigkeit wählen + Wähle aus, mit welchem Token Du die Netzgebühr bezahlen möchtest. %s + Token auswählen Markt & Nachrichten Aktuelle Trends Die folgenden Angaben sind freiwillig. Du kannst diese löschen, wenn du sie nicht weitergeben möchtest. @@ -625,7 +629,7 @@ Trotzdem entfernen! Für diese Wallet existiert ein Backup. Überprüfe dieses bitte, bevor Du sie entfernst, um sicherzustellen, dass Du sie später wiederherstellen kannst. Wenn Du diese Wallet ohne Backup entfernst, verlierst Du dauerhaft den Zugriff auf Deine Assets. - Diese Wallet dauerhaft entfernen? + Bist du sicher, dass du diese Wallet vergessen willst? Mir ist bewusst, dass ich den Zugriff auf meine Wallet verlieren kann, wenn ich sie vor der Entfernung nicht gesichert habe. Mir ist bewusst, dass durch das Entfernen meiner Wallet diese nicht gelöscht, sondern lediglich von meinem Gerät entfernt wird. Upgrade @@ -1474,7 +1478,7 @@ Tippe auf „Karte hinzufügen“. Tippen Sie auf „Debit- oder Kreditkarte“ Gebe die Kartendaten manuell ein - Verifiziere die Karte mit dem Einmalpasswort (OPT), das an Dein Gerät gesendet wird. + Verifiziere Deine Karte mithilfe des an Dein Gerät gesendeten OTP. Alles erledigt! Deine Karte ist einsatzbereit. Karte zu Google Pay hinzufügen Karte zu Apple Pay hinzufügen diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 1c463c9d8e..aad8904c38 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -20,6 +20,8 @@ Archivar Estás archivando esta cuenta, pero siempre puede recuperarla. Cuenta + No se puede editar la cuenta + Algunos tokens personalizados se moverán automáticamente de \"%1$s\" a \"%2$s\" ya que su derivación pertenece a esa cuenta. ¿No encuentra el token en su billetera? Consulte la sección de Mercados para encontrarlo y añadirlo a la compra ¿No encuentra el token en su billetera? Consulte los mercados para encontrarlo y añadirlo a la venta Vender diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 666fe8e6bb..0dbce5c791 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -61,8 +61,8 @@ 新しいアカウントを破棄してもよろしいですか? 編集内容を破棄してもよろしいですか? 保存されていない変更 - 一部のカスタムトークンは、その派生がそのアカウントに属しているため、「 %1$s 」から「 %2$s 」に移動されました。 - 一部のカスタムトークンが移動されました + 一部のカスタムトークンは、その派生元(デリベーション)が該当アカウントに属しているため、自動的に「%1$s」から「%2$s」へ移動されます。 + 一部のカスタムトークンは自動的に移動されます トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して買付できるようにします。 トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して売却できるようにします。 売却 @@ -127,15 +127,15 @@ モバイルウォレット ウォレットのバックアップが正常に完了しました。 - これらの単語は一度失うと復元できません。安全な場所に保管してください。 + これらの単語は、紛失すると復元できません。安全に保管してください。 バックアップが完了しました - シークレットリカバリーフレーズとは、ウォレットへのアクセスや復元のために使う、固定された%s個のランダムな単語のセットです。 + シークレットリカバリーフレーズは、ウォレットへのアクセスおよび復元に使用する、%s個のランダムな単語のセットです。 これらの単語は紛失した場合、復元できません。大切に保管してください。 安全に保管してください これらの%s個の単語を安全な場所に保存し、決して誰とも共有しないでください。 復元は不可能です リカバリーフレーズ - これらの単語は誰にも教えないでください。Tangemがこれらの単語を尋ねることは、絶対にありません。以下の%s個の単語はウォレットのリカバリーフレーズです。デバイスを紛失した場合、これらの単語を使用してウォレットを復元できます。 + 以下の%s個の単語が、あなたのウォレットのリカバリーフレーズです。これらの単語は絶対に他人と共有しないでください。Tangemがこれらを尋ねることは決してありません。デバイスを紛失した場合、このフレーズを使ってウォレットを復元できます。 これらの%s個の単語を番号順に書き留め、安全かつ他人の目に触れない場所に保管してください ウォレットの安全確保およびリカバリーフレーズの安全なバックアップは、すべてご自身の責任となります。 リカバリーフレーズ @@ -539,6 +539,7 @@ お持ちのカードまたはリングについて教えてください サポートチームの皆さん、こんにちは。 問題について詳しく教えてください。どんな些細なことでも役に立ちます。 + バックアップの問題 以前に有効化されたウォレット 私の提案 カード / リングをスキャンできません @@ -557,7 +558,7 @@ 続行するには、%1sスマートコントラクトに%2sを使用する権限を付与してください 許可を与える 無制限 - バックアップは2枚または3枚のTangemカードを使って作成されます。紛失や破損に備えて、それぞれ別々の安全な場所に保管してください—シードフレーズは不要です。 + バックアップは2〜3枚のTangemカードを使用して作成されます。紛失や破損に備え、カードはそれぞれ安全な別の場所に保管してください。シードフレーズは不要です。 複数のカードでバックアップ Tangemウォレットを追加 あなたの秘密鍵はTangemカード内部で直接生成され、決してカードの外に出ることはありません。 @@ -578,7 +579,7 @@ ウォレットの設定を完了する アクセスコードでアプリを保護して、設定を完了してください。 終了すると、最初からやり直す必要があります。 - セットアップを中止してもよろしいですか? + アクティベーションを終了してもよろしいですか? 暗号資産をオフラインで厳重保管。カードサイズで、金庫以上の安心を。 実行すると、最初からやり直す必要があります。 Googleドライブのバックアップから既存のウォレットを復元する @@ -628,7 +629,7 @@ 操作中にエラーが発生しました。 処理中も、資金は安全に保たれ、常にアクセス可能です。 資金へのアクセス - ウォレットのデータはアプリから削除され、ハードウェアウォレットに保存されます + ウォレット情報はアプリから削除され、ハードウェアウォレットに保存されます セキュリティ全般 秘密鍵はアプリからTangemハードウェアウォレットへ移行されます キーの移行 @@ -802,6 +803,7 @@ クイックまとめ 関連ニュース + 関連トークン 情報源 最新情報を入手 お使いのデバイスではNFCが使用できません @@ -884,8 +886,8 @@ 操作を繰り返してください。カードは工場出荷時の設定にリセットされます。 アクティベーションに失敗しました トークンを追加する - バックアップカードまたはリングを1つ追加しました。バックアップ処理が終了すると、これ以上バックアップデバイスを追加することはできません。もう一つカードまたはリングがある場合、バックアップに追加してください。バックアップ処理を続けますか? - バックアップ手続きは部分的に完了しています。現在終了することはできません。 + バックアップ用のカードまたはリングを1つ追加しました。バックアップが完了すると、これ以上デバイスを追加することはできません。追加のカードまたはリングをお持ちの場合は、今のうちに追加してください。続行しますか? + バックアップは一部完了しており、現在は中断できません。 パスフレーズは、リカバリーフレーズに単語やフレーズを追加することで、追加の保護を提供する任意のセキュリティ機能です。これにより、新しいウォレットアドレスのセットが作成されます。 バックアップ用のカードまたはリングを追加 カードをスキャン @@ -933,7 +935,7 @@ レガシー シードフレーズを正しく書き込んだかどうかを確認するために、2番目、7番目、11番目の単語を入力してください。 では、確認してみましょう - バックアップ処理を開始するには、最大2つのバックアップカードまたはリングを追加します。 + バックアップを開始するには、最大2枚のバックアップカードまたはリングを追加してください。 カードまたはリングをもう1つ追加するか、バックアップ処理を完了します 番号%sのバックアップカードを準備してください プライマリカードまたはリングをスキャンして、バックアップ処理を開始します。 @@ -1068,8 +1070,8 @@ この操作を実行すると、現在のウォレットにアクセスできなくなることを理解しています。 このカードを使用して、現在のウォレットの他のカードのアクセスコードを回復させられないことを認識しています。 復元は不可能であり、Tangem Payカードおよびカード上のすべての資金へのアクセスを完全に失うことを理解しました - 工場出荷時設定にリセットすると、選択したカードやリングからウォレットが完全に削除されます。現在のウォレットを復元したり、カードやリングを使用してアクセスコードを復元することはできません。 - 工場出荷時の状態にリセットすると、選択したカードやリングからウォレットが完全に削除され、アプリから削除されます。現在のウォレットを復元することはできません。 + 初期化を行うと、選択したカードまたはリングからウォレットは完全に消去されます。現在のウォレットを復元することはできず、このカードまたはリングを使ってアクセスコードを復旧することもできません。 + 初期化を行うと、選択したカードまたはリングからウォレットが完全に消去され、アプリからも削除されます。現在のウォレットは復元できません。 すべてのTangemデバイスがリセットされました。 アクティベーション処理中に問題が発生しました。カードを1枚ずつリセットしてください。 カード認証に失敗しました @@ -1232,6 +1234,7 @@ ステーキングアカウントとは、ステーキングされたSOLが保管される特別なアカウントです。トークンをバリデーターに委任し、取引の検証に参加して報酬を受け取る際に作成されます。ステーキングアカウントの作成には少額の手数料がかかりますが、ステーキング完了後に返金されます。 年率 APR:1年間で得られる利息を単利で表示します。得た利息は残高に加算されず、その利息にはさらに利息がつきません。 + 年利(APY) APY:1年間で得られる合計利息を複利込みで表示します。複利とは、得た利息が残高に加算され、その利息にもさらに利息がつくことを意味します。 APR APY @@ -1353,7 +1356,7 @@ あなたのステーキング カードまたはリング内に秘密鍵を保管しながら、暗号資産を安全に保管します 革新的なハードウェアウォレット - 1つのウォレットに最大3枚のカードまたは3本のリング + 1つのウォレットに、最大3枚のカードまたはリングを追加 安全なバックアップ ビットコイン、イーサリアム、その他多くの暗号資産を同時に管理できるハードウェアウォレット – オール・イン・ワンのカード / リング 数千の通貨 @@ -1508,7 +1511,9 @@ 技術的な問題を修正しています。後でもう一度お試しください。 サービスは一時的に利用できません 現在、データを表示できませんが、カードでのお支払いは引き続きご利用いただけます。 - 同期が必要です + \nPINコードの設定 + 未同期 + アクセスを復元 日常の支払いにUSDCを利用 Tangem Payは現在一時的に利用できません。 Tangem Pay @@ -1717,7 +1722,7 @@ %sを使用するか、カード / リングをスキャンしてウォレットにアクセスしてください 許可付与のプロセスは現在進行中であり、まもなく完了する予定です。 承認中 - カードまたはリングのアクティベーションが正しく完了しませんでした。デバイスのNFCモジュールに問題があるか、カードまたはリングをデバイスに正しくタップしていないことが原因かもしれません。サポートチームにお問い合わせください。 + アクティベーションが正常に完了しませんでした。NFCの問題、またはタップ方法が正しくない可能性があります。サポートチームまでお問い合わせください。 アクティベーションに失敗しました 2024年12月3日、BEP-2ネットワークはネットワーク開発者の決定により使用不能となり、サポートは終了しました。 BNBビーコンチェーンは閉鎖されました @@ -1728,6 +1733,8 @@ はい、わかりました! すごくクールだ! リフレッシュ + Cloreの公式ドキュメントによると、12月21日以前に受け取ったすべてのコインは Clore(ERC-20トークン)へ移行されますが、同日以降に受け取ったコインは移行されません。送金(移行)ソリューションは現在準備中です。続報をお待ちください。 + Cloreネットワークの移行 現在デモモードです デモモードが有効になっています スキャンしたカードは開発者カードです。ウォレットの作成には使用しないでください。 @@ -1775,7 +1782,7 @@ 現在、ネットワークにアクセスできません。しばらくしてからもう一度お試しください。 ネットワークにアクセスできません ウォレットに入金 - ウォレットがバックアップされていません。アセットを保護するために今すぐこの手順を実行してください。 + ウォレットはまだバックアップされていません。今すぐバックアップして資産を保護しましょう。 バックアップがありません このカードは以前取引に使用されたことがあります。信頼できない出所から受け取った場合は、全資金を引き出すことを検討してください。あなたのカードであれば、何もする必要はありません。 カードはすでに取引に署名済みです @@ -1914,10 +1921,10 @@ 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 ee6d3b99d7..aa04e8ffcf 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -9,6 +9,8 @@ Код доступа может быть легко угадан Введите код доступа Неверный код доступа. Ваш мобильный кошелёк будет удалён после ещё %s неверных попыток. + Неверный код доступа. Приложение будет заблокировано после ещё %s неудачных попыток. + Неверный код доступа.\nПожалуйста, подождите %s секунд и попробуйте снова. Подтвердите ваш код доступа, чтобы продолжить. Повторите код доступа Установите %s-значный код доступа для разблокировки кошелька. @@ -633,6 +635,7 @@ Забыть этот кошелек? Я понимаю, что если я не создал резервную копию кошелька перед его удалением, я могу потерять к нему доступ. Я понимаю, что удаление моего кошелька не стирает его — оно просто удаляет его с моего устройства. + Апгрейд Фраза восстановления больше не нужна — ваша карта или кольцо Tangem становятся вашей надёжной резервной копией. Резервное копирование Это устройство не может быть использовано для апгрейда, оно уже содержит другой кошелек. @@ -814,6 +817,23 @@ Обновитесь до версии %1$s, чтобы создать мобильный кошелёк Мобильный кошелек требует %1$s или новее. Все новости + Нравится + + %dч назад + %dч назад + %dч назад + %dч назад + + + %dмин назад + %dмин назад + %dмин назад + %dмин назад + + Резюме + Связанные новости + Связанные токены + Источники Будьте в курсе Функция NFC недоступна на вашем устройстве О NFT 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 6df04c2899..016bb09e5e 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1,5 +1,67 @@ + Ваш гаманець не захищений без коду доступу. + Все одно пропустити + Код доступу не встановлено + Змінити код + Ваш код доступу буде використовуватися для розблокування гаманця та захисту доступу до ваших активів. + Використати все одно + Код доступу може бути легко вгаданий + Введіть код доступу + Неправильний код доступу. Ваш мобільний гаманець буде видалено після ще %s невірних спроб. + Неправильний код доступу. Додаток буде заблоковано після ще %s невдалих спроб + Неправильний код доступу.\nБудь ласка, зачекайте %s секунд і спробуйте ще раз. + Підтвердьте свій код доступу, щоб продовжити + Повторно введіть код доступу + Встановіть %s-значний код доступу для розблокування вашого гаманця. + Створити код доступу + Код доступу + Не можна створити більше ніж %1$s акаунтів. Архівуйте, щоб додати новий. + Неможливо додати новий акаунт + Акаунт архівовано + Архівовані акаунти + Відновити + Ви збираєтеся відновити \"%1$s\". + Відновити акаунт + Ви досягли ліміту в 20 активних акаунтів. Архівуйте акаунт, щоб відновити цей. + Неможливо відновити акаунт + Архівовано + Не вдалося архівувати акаунт. Будь ласка, спробуйте пізніше. + Цей акаунт бере участь у реферальній програмі. + Цей акаунт не можна архівувати + Не вдалося створити акаунт. Будь ласка, спробуйте пізніше. + Акаунт створено + Архівний акаунт + Архів + Ви архівуєте цей акаунт, але ви завжди можете його відновити. + Архівування... + Акаунт + Неможливо відредагувати обліковий запис + Акаунт збережено + Акаунт для винагород + Акаунт №%s — використовується для деривації. + Додати акаунт + Зберегти + Назва акаунту + Акаунт з такою назвою вже існує. Будь ласка, виберіть іншу назву. + Акаунт + Новий акаунт + Додати акаунт + Редагувати акаунт + Спробуйте пізніше. Якщо проблема повториться, зверніться у підтримку — ми допоможемо її вирішити. + %1$s в %2$s + Основний акаунт + Ви перевищили ліміт %1$s активних акаунтів. Архівуйте, щоб продовжити + Неможливо відновити акаунт + Акаунт відновлено + Натисніть та утримуйте акаунт, щоб змінити порядок акаунтів + Продовжити + Скасувати + Ви впевнені, що хочете скасувати створення нового акаунту? + Ви впевнені, що хочете скасувати зміни? + Незбережені зміни + Деякі користувацькі токени будуть автоматично переміщені з \"%1$s\" до \"%2$s\", оскільки їх похідні належать до того акаунту. + Деякі користувацькі токени будуть автоматично переміщені Токен не знайдено у вашому портфелі? Перевірте Ринки, щоб знайти та додати його для покупки Токен не знайдено у вашому портфелі? Перевірте Ринки, щоб знайти та додати його для продажу Продати @@ -34,12 +96,19 @@ Токени в мережі %1$s не підтримуються цією карткою або кільцем через обмеження прошивки. У вас виникли труднощі зі скануванням картки або кільця? Ця картка не призначена для роботи з цим додатком + Спочатку встановіть код доступу, щоб увімкнути біометрію. + Використовуйте %1$s для розблокування гаманця і підтвердження конфіденційних дій, таких як підписання транзакцій. Для апаратних гаманців для підпису все ще потрібна картка або кільце. Комісія за замовчуванням Увімкніть комісію за замовчуванням, щоб автоматично встановлювати комісію за транзакцію і пропускати сторінку комісій під час відправлення коштів. Ви завжди можете повернутися до цієї сторінки за необхідності. Перейдіть до налаштувань, щоб увімкнути біометричну автентифікацію в додатку Tangem Увімкнути біометричну автентифікацію + Вимкнення %1$s вимагатиме введення коду доступу для розблокування додатку та взаємодії з гаманцем. + Пізніше буде необхідно ввести код доступу для безпечного зберігання гаманця. Усі збережені коди доступу будуть видалені. Вам доведеться вводити код доступу при роботі з гаманцем. При відключенні функції збереження гаманця всі до цього збережені гаманці будуть видалені зі застосунку. + Це призведе до видалення всіх збережених кодів доступу до гаманця. Вам потрібно буде ввести код доступу знову, щоб користуватися гаманцем. + Необхідний код доступу + Ця опція вимикає можливість використовувати біометрію для конфіденційних дій. Вам потрібно буде вводити код доступу щоразу, коли ви підписуєте транзакцію. Зберегти код доступу Підключіть функцію збереження кодів доступу від карток на телефоні у зашифрованому виді, і при роботі з карткою або кільцем замість коду доступу буде запитуватися біометрична автентифікація. Зберігати гаманець у додатку @@ -49,6 +118,16 @@ Системна Тема Налаштування застосунку + Додати гаманець + Виберіть гаманець для входу + З поверненням! + + %s картка + %s картки + %s карток + %s картки + + Мобільний гаманець Ви успішно створили резервну копію свого гаманця. Ці слова неможливо відновити у разі втрати. Зберігайте їх надійно. Бекап завершено @@ -68,10 +147,17 @@ Баланси приховані Згідно інформації від розробників мережі, токени Kaspa знаходяться у режимі бета. Слідкуйте за оновленнями! Бета режим + Біометрія на вашому пристрої вимкнена, тому ви не можете використовувати її для розблокування гаманців. Увімкніть біометрію в налаштуваннях пристрою, щоб знову використовувати цей метод. + Біометрична автентифікація відключена Будь ласка, відскануйте картку або кільце + Ви досягли ліміту спроб біометрії. Розблокуйте гаманець за допомогою дотику пристрою або введіть код доступу. + Біометрична автентифікація заблокована Будь ласка, спробуйте знову через 30 секунд або відскануйте картку або кільце + Біометричний вхід тимчасово заблоковано. Будь ласка, повторіть спробу через 30 секунд або розблокуйте гаманець дотиком пристрою чи кодом доступу. Забагато спроб Ви вимкнули біометричну автентифікацію на своєму телефоні і не зможете зберігати гаманці в додатку. Щоб зберегти гаманці, будь ласка, увімкніть функцію біометричної автентифікації в налаштуваннях телефону. + Біометричні дані на вашому пристрої було оновлено. Будь ласка, оберіть свій гаманець і введіть код доступу до нього, щоб знову увімкнути біометричний вхід. + Увага Під час обробки промокоду сталася помилка. Будь ласка, спробуйте пізніше. Помилка активації Ваш промокод було успішно активовано. Винагороду буде зараховано на ваш рахунок протягом 14 днів. @@ -90,6 +176,19 @@ %d пристроїв %d пристроїв + + %1$d токен + %1$d токени + %1$d токенів + %1$d токени + + Будь ласка, скиньте налаштування наступного пристрою, щоб продовжити. + Скидання гаманця + Всі пристрої Tangem було скинуто. Тепер ви можете продовжити оновлення гаманця. + Оновити ще раз + Скидання завершено + Ми рекомендуємо завершити процес скидання для всіх пристроїв Tangem у цьому гаманці. + Деякі пристрої Tangem все ще потрібно скинути. Вимкніть цю опцію, якщо не хочете, щоб ця картка використовувалася для скидання кодів доступу на інших картках у цьому гаманці. Зверніть увагу, скидання коду доступу також не буде доступним на цій картці. Дозволяє використовувати цю картку для скидання коду доступу на інших картках у цьому гаманці Відновлення коду доступу @@ -115,19 +214,25 @@ Недостатньо ADA Прийняти Доступ заборонено + Акаунт + Акаунти + Активувати Додати Додати у портфель Додати токен + Додано Адреса Усе Дозволити Сума Аналітика + і Застосовувати Затвердження Підтвердити Увага Доступні мережі + Резервне копіювання Баланс: %s Баланс біометричну автентифікацію @@ -138,15 +243,20 @@ Ви не надали доступ до камери, будь ласка, змініть налаштування конфіденційності Скасувати Змінити + Оберіть акаунт Виберіть дію + Оберіть мережу Оберіть токен Оберіть гаманець Отримати Отримати винагороди Закрити + Незабаром Підтвердити Підключення Звернутися у підтримку + Звернутися до служби підтримки Tangem + Звернутися до служби підтримки Visa Продовжити Конвертувати Копіювати @@ -167,6 +277,7 @@ днів Видалити + Вимкнути Вимкнуто Від\'єднати Готово @@ -186,8 +297,13 @@ Повільно Швидкість та комісія Завершити + Забути + Безкоштовно + З + З %s Синхронізувати адреси Почати + Отримати токен Перейти до провайдера Перейти до токену Зrozуміло @@ -198,16 +314,27 @@ Пізніше Дізнатися більше Залишилося %1$s + Legacy Bitcoin Заблокований + Заблоковані гаманці Основна мережа місяць Комісія мережі Сума відправлення буде зменшена на %1$s (%2$s) для покриття обраного рівня комісії + + %1$s мережа + %1$s мережі + %1$s мереж + %1$s мережі + + Нова адреса + Новини Далі NFT Ні Немає адреси Не додано + Не зараз Зараз ОК Відкрити в браузері @@ -220,10 +347,12 @@ %1$s — %2$s Детальніше Отримати + Рекомендовано Відхилити Оновити Перейменувати Обов\'язково + Скинути Зберегти Зберегти зміни Пошук @@ -238,19 +367,25 @@ Сервер недоступний, спробуйте пізніше Поширити Поділитися посиланням + Сховати Показати більше Підписати Підписати та надіслати + Пропустити + Щось пішло не так Застейкати Стейкінг Почати Продовжити Успіх Підтримка + Підтримувані мережі Обмін + Tangem умови участі Умовами використання До + На %s Сьогодні %d токен @@ -262,17 +397,23 @@ Статус транзакції Транзакції Переказ + Неможливо завантажити дані... Я зрозумів + Я розумію, продовжити Виникла помилка. Будь ласка, спробуйте ще раз. Недоступно Скасувати стейкінг Через обмеження %1$s в одну транзакцію може поміститися тільки %2$d UTXO. Це означає, що ви можете відправити тільки %3$s або менше. Вам потрібно зменшити суму. + Скопійовано тиждень з Так Режим дохідності Адреса контракту скопійована! Доступні мережі + Деривація вашого токена співпадає с деривацією %1$s. Ваш токен буде додано до цього акаунта. + Деривація належить іншому акаунту. + Токен додано до акаунту %1$s Адреса контракту Адреса контракту недійсна Будь ласка, оберіть мережу @@ -313,6 +454,7 @@ Переверніть екран пристрою вниз, щоб швидко приховати та відобразити баланси %s хешів Номер пристрою + Відкрити чат підтримки Додати більше карток Валюта застосунку Приховувати баланси жестом перевороту @@ -333,11 +475,13 @@ Помилка WalletConnect Ви використали картку або кільце від іншого гаманця. Прикладіть картку або кільце, пов\'язану з цим гаманцем. Недостатньо коштів для здійснення транзакції. Будь ласка, поповніть свій акаунт. + Обміняйте будь-який актив у вашому портфелі на цей токен Мої токени У вас немає доданих токенів. Додайте токени для обміну Недоступний для обміну з %s Послугу надає Статус + Виберіть провайдера Tangem пропонує обмін токенів через сторонніх провайдерів відповідно до умов кожного провайдера Провайдер Виникла помилка. Код: %s @@ -388,6 +532,7 @@ Найкращий курс Список попереджень FCA Найкращий вибір + Список попереджень FCA Доступно до %s Доступно від %s Недоступно для цієї пари @@ -399,10 +544,15 @@ Приховати цю транзакцію Після приховування статус транзакції більше не можна буде переглянути. Замість цього ви можете просто провести пальцем, щоб закрити. Приховати статус транзакції? + Цей токен не підтримується. Будь ласка, виберіть інший токен для обміну. + %s не підтримується + Обміняти з Токенів не знайдено. Будь ласка, спробуйте інший запит ID: %s ID транзакції скопійовано Обміняйте будь-який актив у своєму портфелі на цей токен + Ринок та Новини + В тренді Інформація нижче не є обов\'язковою. Ви можете стерти її, якщо бажаєте. Розкажіть, яких функцій вам не вистачає, і ми спробуємо вам допомогти. Розкажіть, будь ласка, яку картку або кільце ви маєте? @@ -434,9 +584,11 @@ Генерація ключа Операції з криптографією відбуваються всередині захищеного чіпу, стійкого до клонування та фізичному взлому. Безпека на апаратному рівні + Додати існуючий гаманець Створити новий гаманець Купити Сканувати + Дозволити додатку \"Tangem\" використовувати біометричну автентифікацію для підтвердження вашої особи та відкриття додатку? в %s В мережі %s Ви впевнені, що хочете скасувати процес створення коду доступу? @@ -488,6 +640,7 @@ Забути цей гаманець? Я розумію, що якщо я не створив резервну копію гаманця перед його видаленням, я можу втратити досту до нього. Я розумію, що видалення мого гаманця не видаляє його повністю, а просто видаляє його з мого пристрою. + Оновлення Фраза відновлення більше не потрібна — ваша картка або кільце Tangem стає вашою безпечною резервною копією. Резервне копіювання з Tangem Цей пристрій не може бути використаний для оновлення, він вже містить інший гаманець. @@ -554,17 +707,21 @@ Обраний токен наразі недоступний для дій у криптогаманці. Але не хвилюйтеся, ви можете висловити свою зацікавленість, проголосувавши за його інтеграцію. Проголосувати Гаманець не підтримує більше однієї мережі + Про монету Щоб купити, обміняти або отримати цей актив, додайте його до свого портфоліо Цей актив наразі не підтримується в гаманці Цей токен недоступний для цього гаманця Додати + APY %s Моє портфоліо Маркет + Заробляйте з Tangem Щоб згенерувати адреси для обраних мереж, відскануйте вашу картку або кільце Tangem гаманця Щоб додати токени, потягніть вгору або торкніться панелі пошуку Дані цього розділу отримані з наступних мереж: %s Не вдалося завантажити дані... Немає даних + Пульс ринку Швидкі дії Шукати на маркеті Результат @@ -586,8 +743,10 @@ Лідери росту Лідери падіння В тренді + Режим дохідності Стейкінг — простий спосіб отримувати дохід з вашої криптовалюти. %s Отримуйте до %s APY + Токен додано Про %s %d біржа @@ -658,6 +817,25 @@ Обсяг Потягніть вгору або торкніться панелі пошуку, щоб додати токени безпосередньо з маркету Додати токени + Збільшуйте дохід з активів, зберігаючи миттєвий доступ до них. %s + Активувати режим дохідності + Оновіться до версії %1$s, щоб створити мобільний гаманець + Мобільний гаманець потребує %1$s або новіше. + Всі новини + + %1$s годину тому + %1$s години тому + %1$s годин тому + %1$s годин тому + + + %1$s хвилину тому + %1$s хвилини тому + %1$s хвилин тому + %1$s хвилин тому + + Пов\'язані токени + Залишайтеся в курсі подій Функція NFC недоступна на вашому пристрої Про NFT NFT @@ -716,6 +894,11 @@ Обліковий запис одержувача не активовано. Надішліть %s або більше, щоб активувати обліковий запис. Для створення акаунту надішліть кошти на цю адресу Акаунт одержувача не містить трастлайну для активу, що надсилається. + Отримайте $10 в BTC на кожен гаманець. Поспішайте! + Чорна п\'ятниця: знижки до 30% + Купити + Час обмежений! + 1+1: Купи один гаманець — отримай 50% знижку Приєднатися Поділіться промокодом — заробіть 5 USDT з кожної покупки. Ваші друзі отримають знижку 10% на картку Tangem! Отримуй бонуси за кожного друга! @@ -762,12 +945,14 @@ Генеруйте ключі приватно Ваша картка активована та готова до використання Успішно! + Ваш гаманець налаштований і готовий до використання! У цьому випадку вам доведеться почати наново. Ви хочете вийти з процесу активації? Підготовка На картці, яку ви намагаєтеся додати, вже створено інший гаманець. Якщо у вас є кошти на цьому гаманці, будь ласка, виведіть їх, а потім скиньте цю картку до заводських налаштувань і додайте її як резервну. Збережіть ваш гаманець Резервна копія + Біометрія Дізнатися більше про seed-фразу @@ -825,6 +1010,7 @@ Відновлення коду доступу Ідентичні картки Код доступу + Усі пропозиції Доступно з %s Tangem забезпечує доступ к покупці через сторонніх провайдерів згідно з їхніми умовами Пошук за країною @@ -832,13 +1018,28 @@ Інші валюти Популярні фіати Пошук по валюті + Ця транзакція вже була оброблена. Додаткові дії не потребуються. + Шукаємо найвигідніший курс... + Миттєво Використовуючи сервіс покупки, ви погоджуєтесь з %1$s та %2$s + Послуга надається зовнішнім провайдером.\nTangem не несе відповідальності. Сума покупки не може бути більше ніж %s Сума покупки повинна бути не менше %s Для данної валюти немає доступних провайдерів + Найшвидший Оплата з + Спосіб оплати Доступно до %s Доступно від %s + + %s провайдер + %s провайдери + %s провайдерів + %s провайдерів + + Провайдери + Нещодавно використано + Рекомендовано Ви зможете завершити транзакцію у стороннього постачальника, %s Перенаправлення на %s... Наші сервіси недоступні в цій країні @@ -847,8 +1048,21 @@ Будь ласка, виберіть правильну країну, щоб забезпечити точні способи оплати та послуги. Параметри Ви можете закрити цей екран і перевірити статус транзакції на екрані деталей токена. + + до %s дня + до %s днів + до %s днів + до %s днів + + %s хв + Доступно з + Доступно до + Ви отримаєте + Послуга надається зовнішнім провайдером. \nTangem не несе відповідальності. Ви можете перевірити статус транзакції на сторінці токена + До Через + Ви заплатите Групами За балансом Сортування токенів @@ -860,6 +1074,7 @@ Налаштування Увімкнути сповіщення Отримуйте сповіщення про вхідні транзакції в підтримуваних мережах. + Сповіщення про транзакції Виберіть з галереї Налаштування Ви не надали доступ до своєї камери @@ -871,6 +1086,7 @@ Надсилайте лише %s на цю адресу. Надсилання будь-якої іншої валюти призведе до її незворотної втрати. Надсилайте лише %1$s у мережі %2$s Переказуйте кошти з будь-якого гаманця або біржі + Адреса для отримання винагород Взяти участь Не вдалося завантажити інформацію по реферальній програмі. Будь ласка, спробуйте пізніше. Не вдалося завантажити інформацію по реферальній програмі. Код помилки: %s. Будь ласка, спробуйте пізніше. @@ -909,8 +1125,14 @@ Я розумію, що не можу використати цю картку для відновлення свого коду доступу на інших картках поточного гаманця Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець або використати цю картку або кільце для відновлення коду доступу. Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець. + Усі пристрої Tangem було скинуто. + Щось пішло не так у процесі активації. Будь ласка, скиньте картки по черзі. + Не вдалося перевірити картку + Будь ласка, скиньте наступний пристрій, щоб продовжити Власникам кілець - 3 обміни без комісії на Changelly до 15.11! Обмін з 0% комісією! + Пристрої з root-доступом вважаються менш безпечними. Ваші дані можуть бути піддані додатковим ризикам. + Виявлено root-доступ Увійдіть у додаток та слідкуйте за своїм балансом без сканування картки чи кільця Доступ до додатку Використовувати біометрію @@ -938,6 +1160,7 @@ Транзакція не завершена Конвертувати в інший токен Сума + Буде надіслано одержувачу Ви можете встановити розмір комісії за транзакцію, налаштувавши значення в полі Satoshi per vByte. Комісія, яка буде стягнута за вашу транзакцію. Ви можете встановити власне значення. Максимальна комісія @@ -962,6 +1185,7 @@ Перевірте підключення до мережі Інформація щодо комісії в мережі недоступна Ви надсилаєте + З %s Ліміт газу Це максимальна кількість газу, яка буде витрачена на здійснення транзакції чи контракту. Ліміт газу запобігає несподіваним або необмеженим стягненням під час виконання транзакції. Вартість газу @@ -969,6 +1193,7 @@ Все Максимальна сума Комісія може сягати до + Мемо: %s Недопустимий Memo Покриття мережевої комісії Nonce @@ -1018,6 +1243,7 @@ комісія мережі буде покрита, використовуючи %1$s енергію Комісія мережі буде зменшена, за рахунок використання %1$s енергії включно з комісією мережі %1$s + На адресу Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час %1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron (TRX) на свій рахунок. Сума перевищує залишок @@ -1047,14 +1273,21 @@ Це призведе до видалення гаманця з застосунку. Сам гаманець можна додати знову. Ім\'я Змусьте свій токен працювати + Мережева комісія — це невелика оплата, необхідна для обробки та підтвердження вашої транзакції в блокчейні. + Щоб почати стейкінг, ваш TON-акаунт потрібно активувати транзакцією на 1 TON. Кошти залишаються на вашому акаунті, оскільки цей крок лише активує його для стейкінгу. + Активація акаунту + Мережева комісія змінилася. Будь ласка, перевірте нову суму перед тим, як продовжити. + Комісія мережі оновлена Сума для стейкінгу має бути не менше %s Сума стейкінгу буде округлена до %1$s TRX відповідно до правил мережі. Сума зняття зі стейкінгу буде округлена до %1$s TRX через мережеві правила. + APR %1$s%% Зняти кошти Комісія за стейкінг-акаунт Стейкінг-акаунт — це спеціальний рахунок, на якому зберігаються застейкані SOL токени. Він створюється, коли ви делегуєте свої токени валідатору для участі у перевірці транзакцій та отримання винагород. За створення стейкінг-акаунту стягується невелика комісія, яка повертається після завершення стейкінгу. Процентна ставка APR: Показує відсоток, який ви можете заробити за рік без урахування складного відсотка. Нараховані відсотки не додаються до вашого балансу, тому на них не нараховуються додаткові відсотки. + Річна відсоткова дохідність APY: Показує загальний відсоток, який ви можете заробити за рік з урахуванням складного відсотка. Складний відсоток означає, що нараховані відсотки додаються до вашого балансу, і на них також нараховуються відсотки. APR APY @@ -1083,6 +1316,7 @@ Комісія за схвалення токену, щоб верифікувати, що саме ви дозволяєте використання токена для стейкінгу. Використовуючи функцію стейкінгу, ви погоджуєтесь з %1$s та %2$s Заблоковано + Максимальна сума: %s Перемістити Нативний стейкінг На даний момент немає доступних активних валідаторів для стейкінгу. Будь ласка, спробуйте пізніше. @@ -1113,6 +1347,7 @@ Рестейк дозволяє вам перемістити ваші кошти від одного валідатора до іншого без необхідності виводити кошти зі стейкінгу Ви збираєтеся застейкати весь свій баланс. Ми рекомендуємо залишити невелику суму, щоб покрити комісію мережі за зняття коштів або отримання винагороди. Щоб розпочати стейкінг, спочатку активуйте свій TON-акаунт. + Активація акаунту Щоб почати стейкінг в TON, спочатку здійсніть вихідну транзакцію на будь-яку суму — це активує ваш гаманець. До 0.2 TON може знадобитися додатково мережевої комісії для завершення транзакції. Невикористана частина буде повернута. Для виконання операції потребується додатково 0.2 TON, крім мережевої комісії. Будь ласка, поповніть баланс. @@ -1186,6 +1421,7 @@ Для відправки потрібна вхідна транзакція на суму не менше %1$s Недостатньо коштів Підтверджуючи, ви дозволяєте смартконтракту використовувати ваші токени в майбутніх транзакціях. + Фіксований курс Мережа стягує комісію за схвалення токену за підтвердження, що саме ви дозволяєте використовувати ваш токен для обміну. Обмінюйте більше токенів за вигіднішим курсом прямо у своєму гаманці. З\'явився новий провайдер обмінів! @@ -1210,33 +1446,130 @@ Помилка при розрахунку комісії. Будь ласка, надішліть відгук до служби підтримки. Ви обмінюєте Обмін цієї кількості обраних токенів призведе до значного впливу на ціну і зменшить вашу кінцеву суму. + Високий вплив на ціну Недостатньо коштів Надати дозвіл Обміняти Ви отримаєте Оберіть токен недоступно + Будемо раді вашому зворотньому звʼязку + Tangem Pay у режимі beta + Картку заморожено + Оплата карткою + Депозит + Оскаржити + Переглянути транзакцію + Сервісна комісія + Комісія + Зберігайте свої гроші в безпеці. Ви можете розморозити картку у будь-який час. + Заморозити вашу картку? + Не вдалося заморозити картку. Спробуйте пізніше. + Заморозити + Вашу картку заморожено. + Звернутися до підтримки + Інше + Завершено + Відхилено + В очікуванні + Умови, комісії та ліміти + Умови та обмеження + Банк відхилив цей запит на транзакцію. + Ця комісія покриває витрати на обробку вашого переказу. + Продовжуйте користуватися карткою. Заморозити можна в будь-який момент. + Розморозити картку? + Не вдалося розморозити картку. Спробуйте пізніше. + Картку розморожено. + Виведення коштів + Заборонено використовувати на root-пристроях + Скасувати KYC + Поповнити рахунок + Варіанти поповнення Номер картки + Змінити PIN-код + Картка готова до оплат. + PIN-код створено CVC + Не вдалося завантажити дані. Спробуйте пізніше. Термін дії + Заморозити картку Сховати деталі + Приховати + Відкрити Google Wallet + Налаштуйте Tangem Pay в кілька кроків і почніть платити за допомогою Google Pay. + Налаштуйте Tangem Pay в кілька кроків і почніть платити за допомогою Apple Pay. + Додайте картку до Google Pay Додайте свою картку в Apple Pay + Відкрийте Google Wallet Натисніть кнопку \"+\" у верхньому правому куті Відкрити Apple Wallet + Натисніть «Додати картку» Натисніть «Дебетова або кредитна картка» + Введіть дані картки вручну + Підтвердьте картку за допомогою одноразового коду, надісланого на ваш пристрій. Все готово! Ваша картка готова до використання. + Додайте картку до Google Pay Додайте свою картку в Apple Pay + ПІН-код + Поділіться своєю адресою або покажіть QR-код + Виявлено технічні проблеми. Будь ласка, спробуйте пізніше або зверніться до служби підтримки. + Поповнення наразі недоступне + Показати Показати деталі Обміняйте будь-який актив у вашому портфелі на картку + Реквізити картки Розморозити картку + Ваш ПІН + Вивести + Вивід наразі недоступний + Ви не можете розпочати обмін або новий вивід, поки не завершено поточний. + Вивід виконується + Змінити PIN-код + Можна подивитися тут, якщо забудете його. + Я розумію, що повністю втрачу доступ до своєї картки Tangem Pay та всіх коштів на ній без можливості відновлення. + Не вдалося випустити картку + Технічна помилка, спробуйте ще раз, натиснувши кнопку нижче + Технічна помилка, звʼяжіться з підтримки Отримайте безкоштовну віртуальну картку Tangem Visa + Отримати Tangem Pay + Написати у підтримку + Зазвичай це займає до 15 хвилин + Готуємо вашу картку Tangem + Випускаємо картку + Випускаємо картку, це займе деякий час. + Tangem Pay + Підтвердити скасування + Ви впевнені, що хочете зупинити процес KYC? Ви можете повернутися до нього в будь-який момент. + Не вдалося підтвердити ваш профіль. Якщо у вас є питання, зверніться у службу підтримки. + На жаль, нам не вдалося підтвердити вашу особу + KYC в процесі + Переглянути статус + Перевірка KYC для Tangem Pay в процесі + Ви можете переглянути поточний статус KYC або відмінити його Отримайте безкоштовну віртуальну картку Tangem Visa Використовуйте USDC для щоденних платежів + Отримати картку + Віртуальну картку можна додати до Apple Pay та Google Pay + Витрачайте свої активи будь-де + Немає жодних додаткових комісій за покупки + Платіть стільки, скільки бачите + Буде створено окремий платіжний рахунок без розкриття ваших адрес та активів + Неперевершена конфіденційність + Отримайте безкоштовну картку Tangem Pay за лічені хвилини + Платіжний акаунт Платіжний рахунок не синхронізовано + Слабкий ПІН: не використовуйте повторів або послідовностей. Ми усуваємо технічну проблему. Будь ласка, спробуйте пізніше. Сервіс тимчасово недоступний + Не можемо показати дані картки, але оплати продовжують працювати. + Встановіть \nPIN-код + Не синхронізовано + Відновити доступ Використовуйте USDC для щоденних платежів + Tangem Pay тимчасово недоступний + Tangem Pay Натисніть кнопку нижче, щоб відновити доступ + Ваш PIN-код Це мій гаманець Баланси приховано Баланси показано @@ -1265,6 +1598,7 @@ Токен у мережі %%image%% %1$s Токен %1$s (%2$s) є основною валютою в мережі %3$s і не може бути прихований до тих пір, поки у вас в списку є інші токени цієї мережі Неможливо приховати %s + Показати QR-код Обмінюйте цей токен на інші з %1$s комісією за обслуговування з %2$s по %3$s лютого. Обмін із Changelly, %s комісії Обміняти @@ -1279,6 +1613,7 @@ Кілька адрес Історія транзакцій наразі не підтримується для цього блокчейну. Але не хвилюйтеся, ми працюємо над цим! А поки можете перевірити це в оглядачі. Операція + для: %s від: %s до: %s валідатор: %s @@ -1301,6 +1636,7 @@ Ця дія є незворотною. Ви не матимете доступу до старого гаманця. Прикладіть twin-картку з номером %s та не прибирайте її до завершення операції Спробуйте ще раз пізніше. Якщо проблема залишиться, зверніться до служби підтримки. + Щось пішло не так! Виникла помилка. Код помилки: %s. Спробуйте, будь ласка, знову. Якщо проблема знову виникає — зверніться до нашої служби підтримки. Використовуйте %s або відскануйте картку/кільце, щоб отримати доступ до свого гаманця Не вдалося встановити з\'єднання: Цей dApp використовує Wallet Connect версії 1.0, яка не підтримується. Будь ласка, переконайтеся, що dApp підтримує Wallet Connect версії 2.0 для успішного підключення. @@ -1312,6 +1648,8 @@ Ранній доступ до нових функцій та ексклюзивних пропозицій. Нові функції та важливі новини Бажаєте використовувати Push-повідомлення? + Увімкніть push-сповіщення, щоб отримувати повідомлення, коли кошти надходять на ваш гаманець. + Не пропустіть транзакцію Додати новий гаманець Якщо ви видалите цей гаманець без резервної копії, ви назавжди втратите доступ до своїх коштів. Ви впевнені, що хочете видалити цей гаманець? @@ -1322,7 +1660,39 @@ Перейменування гаманця Розблокувати все Розблокувати все з %s + + доступно протягом %1$s дня + доступно протягом %1$s днів + доступно протягом %1$s днів + доступно протягом %1$s днів + + Баланси та ліміти + Код доступу + Активація акаунту + Ви впевнені, що хочете вийти? Ви можете продовжити пізніше з того місця, де зупинилися. + Це не займе багато часу. Ми налаштовуємо ваш акаунт. + Це не займе багато часу. Ми завершуємо активацію. + Встановіть 4-значний код.\nВін буде використаний для платежів. + ПІН-код + Встановити ПІН-код PIN-код не прийнятий. Спробуйте ще раз або введіть інший код. + Слабкий ПІН: не використовуйте повторення або послідовності + Перейти на веб-сайт + Давайте продовжимо налаштування вашого акаунту. + З поверненням! + Виконайте ці кроки, щоб налаштувати свій акаунт. + Вітаємо! + Оскаржити цю транзакцію + Розблокувати + Відскануйте картку, щоб розблокувати доступ + Потрібне розблокування + Оберіть тип гаманця + Відскануйте картку або кільце Tangem, щоб відновити або імпортувати з іншого гаманця. + Створити апаратний гаманець + Хочете придбати гаманець Tangem? + Імпортувати фразу відновлення + Відновіть гаманець на телефоні або імпортуйте з іншого додатку — зручно, але менш безпечно, ніж картка Tangem. + Що обрати? Блокчейн недоступний. Спробуйте пізніше Відскануйте картку або кільце Цей гаманець вже був активований раніше.\nЯкщо це було зроблено не вами, зверніться до служби підтримки.\nTangem ніколи не продає гаманці разом з попередньо згенерованим кодом доступу. @@ -1343,6 +1713,8 @@ Невірна картка або кільце обрана в додатку Tangem Не вдалося створити транзакцію з даних Dapp. Код: %s Ми зіткнулися з невідомою помилкою. Код помилки: %d. Якщо проблема не зникає — зверніться до нашої служби підтримки. + Багатоетапна транзакція + Для успішної обробки вашу транзакцію буде розділено на кілька частин. Потрібно буде кілька разів прикласти картку, щоб завершити її. Немає відкритих сеансів WalletConnect Упс. Немає сеансів. Не вдалося створити пару WalletConnect: %1$s @@ -1354,10 +1726,36 @@ Цю картку не можна використовувати з WalletConnect. Ця мережа не підтримується. Будь ласка, виберіть іншу мережу. Оберіть мережу + Транзакція обробляється. Будь ласка, прикладіть картку кілька разів, щоб завершити її. + Транзакція обробляється Сеанси WalletConnect Підключення до dApps WalletConnect Підключення може зайняти кілька секунд + Створіть гаманець Tangem + Від %s + Купіть Tangem Wallet — фізичний пристрій, який надійно зберігає ваш приватний ключ офлайн. + Апаратний гаманець + Миттєво створіть безпечний гаманець на телефоні + Мобільний гаманець + Що вибрати + Вже користуєтеся Tangem Wallet? + Відсканувати + Виберіть спосіб створення гаманця + Хочете купити гаманець Tangem? + Купити зараз + Відновіть існуючий гаманець за допомогою резервної копії Google Drive + Імпорт з Google Drive + Додати існуючий гаманець + Фізичні пристрої, які надійно зберігають ваш приватний ключ офлайн. + Відскануйте гаманець Tangem + Імпортуйте існуючий гаманець за допомогою сід-фрази. + Імпортувати гаманець + Введіть фразу відновлення + Ви успішно створили резервну копію свого гаманця. + Імпортувати гаманець + Імпорт завершено + Імпортувати гаманець Ринкова ціна %s за 24 години Мережа %s @@ -1366,8 +1764,13 @@ Отримати з 10% знижкою Доступ до 13,000+ криптовалют. Купуйте, продавайте, обмінюйте та стейкайте одним дотиком.\nЗ’єднайте до трьох карток для бекапу. Відкрийте Tangem Wallet + Цей код доступу захищає ваш гаманець і використовується для входу та підписання транзакцій. + Встановити/Змінити код доступу + Змінити код доступу Отримуйте сповіщення про вхідні транзакції в гаманці та оновлення Tangem. + Push-повідомлення наразі можуть не працювати на пристроях Huawei. Ми активно працюємо над розв\'язанням цієї проблеми та плануємо випустити виправлення в наступному оновленні. Дякуємо за розуміння! Сповіщення про транзакції + Встановити код доступу Налаштування гаманця Tangem Використовуйте %s або відскануйте картку/кільце, щоб розблокувати доступ до вашого гаманця @@ -1443,6 +1846,7 @@ Ваш відгук мотивує нас робити гаманець Tangem Wallet ще кращим Подобається Tangem? Вам необхідно провести асоціацію токена, щоб мати можливість приймати його + Ви повинні відкрити трастлайн для свого токена, щоб мати можливість отримати на нього кошти Необхідна плата за оренду мережі Потрібна дія Чи зверталися в службу підтримки через додаток протягом 7 днів після створення гаманця? Якщо зверталися або не впевнені, натисніть «Так» і дотримуйтесь інструкцій. @@ -1468,6 +1872,7 @@ Відкрийте Trustline Потрібна мережа %s не додана до вашого портфеля. Спочатку додайте її, а потім виконайте підключення. Додати мережу до портфеля + Зловмисний домен Невідомий домен Все одно підключитися Помилка тайм-ауту. Будь ласка, спробуйте пізніше. @@ -1479,9 +1884,14 @@ Час очікування запиту вичерпано Будь ласка, поверніться до браузеру і повторно підключіться через WalletConnect. Сеанс Wallet Connect було завершено + Підписати все одно Код помилки: %s. Якщо проблема зберігається, зверніться до нашої служби підтримки. + Якщо проблема не зникає — зверніться до нашої служби підтримки. Ми зіткнулися з невідомою помилкою Tangem Wallet наразі не підтримує %s + Непідтримуваний dApp + Код помилки: 8 005. Якщо проблема не зникає - зверніться до нашої служби підтримки. + Ми помітили невідому помилку Ця мережа %s не підтримується Tangem Wallet і не може бути підключена. Непідтримувана мережа Tangem наразі не підтримує необхідну мережу для %s. @@ -1491,11 +1901,14 @@ Обрана не вірна картка або кільце Схоже, виникла проблема Усі dApps відключені + Дозвіл на використання Підтверджуючи, ви дозволяєте dApp або смартконтракту використовувати токени в майбутніх транзакціях. Адреса Підключення + Завантаження Мережа Мережі + Необмежено Гаманець Підключений додаток Підключені мережі @@ -1509,6 +1922,7 @@ З\'єднання Вміст Копіювати дані + Користувацький ліміт dApp відключено Розʼєднати все Усі сесії dApp буде відключено. Ваш гаманець більше не буде пов’язаний із жодним dApp. @@ -1520,9 +1934,12 @@ Будь ласка, згенеруйте новий URI та спробуйте ще раз Термін для з’єднання минув Прогнозовані зміни + Не вдалося виконати симуляцію транзакції. Будь ласка, дійте з обережністю. + Оцінка не підтримується для %s Пропоновано %s Поповніть баланс, щоб покрити комісію мережі Недостатньо %1$s + Зловмисна транзакція Додайте %s мережі до вашого портфелю для цього гаманця В гаманці не додані необхідні мережі Нове з\'єднання @@ -1541,7 +1958,25 @@ До Запит транзакції Запит транзакції + Необмежена кількість + Переконайтеся, що при кожній спробі з\'єднання використовується новий та унікальний URI + URI вже використовується WalletConnect + Підозріла транзакція + Вже маєте гаманець Tangem? + Тисячі активів + Революційний апаратний гаманець + Швидка доставка + Почніть в один клік + Просто та надійно + Простий у використанні + Створіть апаратний гаманець з Tangem. Тонкий, як банківська картка, надійний, як банківське сховище. + Створіть або імпортуйте програмний гаманець + Створіть або імпортуйте програмний гаманець на своєму телефоні. + Почніть з Мобільним гаманцем + Інші методи + Використовуйте апаратний гаманець Tangem + Дізнатися більше та придбати Відмовитися Ви не завершили резервне копіювання. Бажаєте продовжити? Так, поновити @@ -1592,7 +2027,10 @@ Політика комісій Tangem комісію за обслуговування у розмірі 15% від отриманого доходу. Ваші кошти будуть автоматично переведені у Aave, як тільки мережеві комісії знизяться або ваш баланс досягне мінімально необхідної суми. + Комісії наразі вищі чим зазвичай із-за високої активності на ринку. Ви можете продовжити зараз чи повернутися пізніше, коли комісія знизиться. + Висока мережева комісія Історична прибутковість + Отримуй до %1$s APY на свій баланс Дозвіл для вашого токена в режимі Yield було відкликано. Відкрийте токен, щоб надати дозвіл знову. Потрібний дозвіл для токену Перевірте підключення до мережі @@ -1607,7 +2045,9 @@ Децентралізований та некастодіальний Використовуючи сервіс, ви погоджуєтесь з умовами провайдера %1$s та %2$s Підключити Aave + Підключіть %1$s %% APY\nна ваш баланс Aave %1$s%% • Плаваюча ставка + Динамічна процентна ставка Аave Серед. %s Дохідність за минулий рік @@ -1626,6 +2066,7 @@ Вимкнення цієї опції виведе ваші активи з Aave, отримаєте їх назад у %s у вашому гаманці і зупинить накопичення нагород. Комісію мережі стягує блокчейн при виході з режиму доходності. Завершити заробіток + Обіг Річна прибутковість (APY) APY Відсотки нараховуються автоматично. @@ -1633,15 +2074,20 @@ Режим дохідності Відправка ваших коштів Режим прибутковості + Встановлення контракту режиму дохідності Режим прибутковості увімкнено %1$s постачено в Aave Режим прибутковості вимкнено %1$s виведено з Aave + Режим дохідності ініціалізовано + Режим дохідності знову активовано Постачити в Aave %1$s постачено в Aave + Вивід із Aave Автоматично Внесіть трохи %1$s %2$s, щоб покрити комісію мережі за транзакції. Неможливо покрити комісію в %s Сервіс доходності у даний час недоступний. Будь ласка, спробуйте пізніше. Режим дохідності недоступний + Не вдалося завантажити графік... diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 582d2ca04c..4833e76320 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -539,6 +539,10 @@ ID: %s Transaction ID copied Swap any asset in your portfolio for this token + Higher speed means faster confirmation and a higher network fee. %s + Choose speed + Choose which token to use to pay the network fee. %s + Choose token Market & News Tangem AI Trending Now diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index a4297a69f1..32c849b301 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { /** Common */ implementation(projects.common.ui) implementation(projects.common.routing) + implementation(projects.common) /** Domain */ implementation(projects.domain.models) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt index 3837c604f8..8b1a940af1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -17,7 +17,7 @@ import com.tangem.features.send.v2.api.FeeSelectorComponent import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams -import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel +import com.tangem.features.send.v2.feeselector.model.FeeSelectorBlockModel import com.tangem.features.send.v2.feeselector.ui.FeeSelectorBlockContent import com.tangem.utils.extensions.isSingleItem import dagger.assisted.Assisted @@ -35,7 +35,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( private val sendFeatureToggles: SendFeatureToggles, ) : FeeSelectorBlockComponent, AppComponentContext by appComponentContext { - private val model: FeeSelectorModel = getOrCreateModel(params = params) + private val model: FeeSelectorBlockModel = getOrCreateModel(params = params) private val bottomSheetSlot = childSlot( source = model.feeSelectorBottomSheet, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt index aae3d79280..807e95bcb3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt @@ -3,23 +3,50 @@ package com.tangem.features.send.v2.feeselector import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.essenty.instancekeeper.getOrCreateSimple 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.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.send.v2.api.FeeSelectorComponent import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams +import com.tangem.features.send.v2.feeselector.component.speed.FeeSpeedSelectorComponent import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel +import com.tangem.features.send.v2.feeselector.route.FeeSelectorRoute import com.tangem.features.send.v2.feeselector.ui.FeeSelectorModalBottomSheet import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +@Suppress("PropertyUsedBeforeDeclaration") internal class DefaultFeeSelectorComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: FeeSelectorParams.FeeSelectorDetailsParams, @Assisted private val onDismiss: () -> Unit, + private val feeSpeedSelectorComponentFactory: FeeSpeedSelectorComponent.Factory, ) : FeeSelectorComponent, AppComponentContext by appComponentContext { - private val model: FeeSelectorModel = getOrCreateModel(params = params) + private val stackNavigation = instanceKeeper.getOrCreateSimple { StackNavigation() } + private val innerRouter = InnerRouter( + stackNavigation = stackNavigation, + popCallback = { model.onChildBack() }, + ) + private val model: FeeSelectorModel = getOrCreateModel(params = params, router = innerRouter) + + private val contentStack = childStack( + key = "FeeSelectorDetailsStack", + source = stackNavigation, + serializer = FeeSelectorRoute.serializer(), + initialConfiguration = model.getInitialRoute(), + handleBackButton = false, + childFactory = ::screenChild, + ) override fun dismiss() { onDismiss() @@ -28,14 +55,38 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor( @Composable override fun BottomSheet() { val state by model.uiState.collectAsStateWithLifecycle() + val contentStack by contentStack.subscribeAsState() + FeeSelectorModalBottomSheet( + childStack = contentStack, onDismiss = ::dismiss, state = state, - feeSelectorIntents = model, + feeSelectorIntents = model.intents, feeDisplaySource = params.feeDisplaySource, ) } + private fun screenChild(config: FeeSelectorRoute, componentContext: ComponentContext): ComposableContentComponent { + val appComponentContext = childByContext( + componentContext = componentContext, + router = innerRouter, + ) + + val componentParams = FeeSelectorComponentParams( + state = model.uiState, + intents = model.intents, + ) + + return when (config) { + FeeSelectorRoute.ChooseSpeed -> feeSpeedSelectorComponentFactory.create( + appComponentContext = appComponentContext, + params = componentParams, + ) + FeeSelectorRoute.ChooseToken -> TODO() + FeeSelectorRoute.NetworkFee -> TODO() + } + } + @AssistedFactory interface Factory : FeeSelectorComponent.Factory { override fun create( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/FeeSelectorComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/FeeSelectorComponentParams.kt new file mode 100644 index 0000000000..cddc26216e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/FeeSelectorComponentParams.kt @@ -0,0 +1,10 @@ +package com.tangem.features.send.v2.feeselector.component + +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents +import kotlinx.coroutines.flow.MutableStateFlow + +internal class FeeSelectorComponentParams( + val state: MutableStateFlow, + val intents: FeeSelectorIntents, +) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/FeeSpeedSelectorComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/FeeSpeedSelectorComponent.kt new file mode 100644 index 0000000000..1735ccd476 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/FeeSpeedSelectorComponent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.send.v2.feeselector.component.speed + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams +import com.tangem.features.send.v2.feeselector.component.speed.model.FeeSpeedSelectorModel +import com.tangem.features.send.v2.feeselector.component.speed.ui.FeeSpeedSelectorContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class FeeSpeedSelectorComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: FeeSelectorComponentParams, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: FeeSpeedSelectorModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + if (state is FeeSelectorUM.Content) { + FeeSpeedSelectorContent( + state = state as FeeSelectorUM.Content, + intents = model, + ) + } + } + + @AssistedFactory + interface Factory { + fun create( + appComponentContext: AppComponentContext, + params: FeeSelectorComponentParams, + ): FeeSpeedSelectorComponent + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/FeeSpeedSelectorIntents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/FeeSpeedSelectorIntents.kt new file mode 100644 index 0000000000..13a9fb4477 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/FeeSpeedSelectorIntents.kt @@ -0,0 +1,15 @@ +package com.tangem.features.send.v2.feeselector.component.speed + +import androidx.compose.runtime.Stable +import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents +import com.tangem.features.send.v2.feeselector.model.StubFeeSelectorIntents + +@Stable +internal interface FeeSpeedSelectorIntents : FeeSelectorIntents { + fun onLearnMoreClick() +} + +@Stable +internal class StubFeeSpeedSelectorIntents : FeeSpeedSelectorIntents, FeeSelectorIntents by StubFeeSelectorIntents() { + override fun onLearnMoreClick() {} +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/model/FeeSpeedSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/model/FeeSpeedSelectorModel.kt new file mode 100644 index 0000000000..8f42d817fb --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/model/FeeSpeedSelectorModel.kt @@ -0,0 +1,39 @@ +package com.tangem.features.send.v2.feeselector.component.speed.model + +import androidx.compose.runtime.Stable +import com.tangem.common.TangemBlogUrlBuilder +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams +import com.tangem.features.send.v2.feeselector.component.speed.FeeSpeedSelectorIntents +import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +internal class FeeSpeedSelectorModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val urlOpener: UrlOpener, +) : Model(), FeeSpeedSelectorIntents, + FeeSelectorIntents by paramsContainer.require().intents { + + private val params = paramsContainer.require() + + val uiState: StateFlow + field = params.state + + override fun onLearnMoreClick() { + modelScope.launch { + urlOpener.openUrl( + TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.WhatIsTransactionFee), + ) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/ui/FeeSpeedSelectorContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/ui/FeeSpeedSelectorContent.kt new file mode 100644 index 0000000000..87eab44b5c --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/ui/FeeSpeedSelectorContent.kt @@ -0,0 +1,513 @@ +package com.tangem.features.send.v2.feeselector.component.speed.ui + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.* +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.components.inputrow.InputRowEnter +import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmountV2 +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SendSelectNetworkFeeBottomSheetTestTags +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.send.v2.api.entity.* +import com.tangem.features.send.v2.feeselector.component.speed.FeeSpeedSelectorIntents +import com.tangem.features.send.v2.feeselector.component.speed.StubFeeSpeedSelectorIntents +import com.tangem.features.send.v2.impl.R +import com.tangem.utils.StringsSigns +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal + +@Composable +internal fun FeeSpeedSelectorContent( + state: FeeSelectorUM.Content, + intents: FeeSpeedSelectorIntents, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.padding( + bottom = 16.dp, + start = 16.dp, + end = 16.dp, + ), + ) { + Title(onLearnMoreClick = intents::onLearnMoreClick) + SpacerH8() + FeeSelectorItems( + state = state, + intents = intents, + ) + } +} + +@Composable +private fun Title(modifier: Modifier = Modifier, onLearnMoreClick: () -> Unit) { + val linkText = stringResourceSafe(R.string.common_learn_more) + val fullString = stringResourceSafe(R.string.fee_selector_choose_speed_description, linkText) + val linkTextPosition = fullString.length - linkText.length + val defaultColor = TangemTheme.colors.text.secondary + val linkColor = TangemTheme.colors.text.accent + val annotatedString = remember(defaultColor, linkColor, onLearnMoreClick) { + buildAnnotatedString { + withStyle(SpanStyle(defaultColor)) { + append(fullString.substring(0, linkTextPosition)) + } + withLink( + link = LinkAnnotation.Clickable( + tag = "learn_more", + linkInteractionListener = { onLearnMoreClick() }, + ), + block = { + withStyle(SpanStyle(color = linkColor, textDecoration = TextDecoration.None)) { + append( + fullString.substring(linkTextPosition, fullString.length).replace( + ' ', + StringsSigns.NON_BREAKING_SPACE, + ), + ) + } + }, + ) + } + } + + Text( + modifier = modifier.padding(horizontal = 32.dp), + text = annotatedString, + style = TangemTheme.typography.caption2, + textAlign = TextAlign.Center, + ) +} + +@Composable +private fun FeeSelectorItems( + state: FeeSelectorUM.Content, + intents: FeeSpeedSelectorIntents, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + val feeFiatRateUM = state.feeFiatRateUM + state.feeItems.fastForEachIndexed { index, item -> + val isSelected = item.isSameClass(state.selectedFeeItem) + val iconTint by animateColorAsState( + targetValue = if (isSelected) TangemTheme.colors.icon.accent else TangemTheme.colors.text.tertiary, + label = "Fee selector icon tint change", + ) + val iconBackgroundColor by animateColorAsState( + targetValue = if (isSelected) { + TangemTheme.colors.icon.accent.copy(alpha = 0.1F) + } else { + TangemTheme.colors.background.secondary + }, + label = "Fee selector icon background change", + ) + val itemModifier = Modifier + .padding(top = 8.dp) + .fillMaxWidth() + .selectedBorder(isSelected = isSelected) + .clickableSingle(onClick = { intents.onFeeItemSelected(item) }) + .background(TangemTheme.colors.background.action) + when (item) { + is FeeItem.Custom -> CustomFeeBlock( + modifier = itemModifier, + customFee = item, + isSelected = isSelected, + iconBackgroundColor = iconBackgroundColor, + iconTint = iconTint, + onValueChange = intents::onCustomFeeValueChange, + nonce = state.feeNonce, + ) + else -> RegularFeeItemContent( + modifier = itemModifier, + title = item.title, + iconRes = item.iconRes, + iconBackgroundColor = iconBackgroundColor, + iconTint = iconTint, + preDot = stringReference( + item.fee.amount.value.format { + crypto( + symbol = item.fee.amount.currencySymbol, + decimals = item.fee.amount.decimals, + ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) + }, + ), + postDot = if (state.feeExtraInfo.isFeeConvertibleToFiat && feeFiatRateUM != null) { + getFiatReference( + value = item.fee.amount.value, + rate = feeFiatRateUM.rate, + appCurrency = feeFiatRateUM.appCurrency, + ) + } else { + null + }, + ellipsizeOffset = item.fee.amount.currencySymbol.length, + ) + } + } + } +} + +@Suppress("LongParameterList") +@Composable +private fun CustomFeeBlock( + customFee: FeeItem.Custom, + isSelected: Boolean, + iconBackgroundColor: Color, + iconTint: Color, + onValueChange: (Int, String) -> Unit, + nonce: FeeNonce, + modifier: Modifier = Modifier, +) { + val textMeasurer = rememberTextMeasurer() + val textHeight = with(LocalDensity.current) { + textMeasurer.measure("S", style = TangemTheme.typography.subtitle2).size.height.toDp() + + textMeasurer.measure("S", style = TangemTheme.typography.caption1).size.height.toDp() + 2.dp + } + + Column(modifier = modifier) { + Row( + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 15.dp) + .testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_FEE_ITEM), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier + .size(36.dp) + .background(color = iconBackgroundColor, shape = CircleShape) + .padding(6.dp) + .testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_ITEM_ICON), + painter = painterResource(R.drawable.ic_edit_v2_24), + tint = iconTint, + contentDescription = null, + ) + Box(Modifier.heightIn(min = textHeight)) { + Text( + text = stringResourceSafe(R.string.common_custom), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + modifier = Modifier + .align(Alignment.CenterStart) + .testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_ITEM_TITLE), + ) + } + } + AnimatedVisibility( + visible = isSelected, + label = "Custom Fee Selected Animation", + enter = expandVertically().plus(fadeIn()), + exit = shrinkVertically().plus(fadeOut()), + ) { + ExpandedCustomFeeItems( + customFeeFields = customFee.customValues, + onValueChange = onValueChange, + nonce = nonce, + ) + } + } +} + +@Composable +private fun ExpandedCustomFeeItems( + customFeeFields: ImmutableList, + onValueChange: (Int, String) -> Unit, + nonce: FeeNonce, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + customFeeFields.fastForEachIndexed { index, field -> + val isShowDivider = index != customFeeFields.size - 1 || nonce is FeeNonce.Nonce + if (field.label != null) { + InputRowEnterInfoAmountV2( + text = field.value, + decimals = field.decimals, + symbol = field.symbol, + title = field.title, + titleColor = TangemTheme.colors.text.tertiary, + info = field.label, + description = field.footer, + keyboardOptions = field.keyboardOptions, + keyboardActions = field.keyboardActions, + onValueChange = { onValueChange(index, it) }, + showDivider = isShowDivider, + isReadOnly = field.isReadonly, + ) + } else { + InputRowEnterInfoAmountV2( + text = field.value, + decimals = field.decimals, + title = field.title, + titleColor = TangemTheme.colors.text.tertiary, + symbol = field.symbol, + description = field.footer, + onValueChange = { onValueChange(index, it) }, + keyboardOptions = field.keyboardOptions, + keyboardActions = field.keyboardActions, + showDivider = isShowDivider, + ) + } + } + + if (nonce is FeeNonce.Nonce) { + InputRowEnter( + text = nonce.nonce?.toString().orEmpty(), + title = resourceReference(R.string.send_nonce), + description = resourceReference(R.string.send_nonce_footer), + onValueChange = nonce.onNonceChange, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + placeholder = resourceReference(R.string.send_nonce_hint), + titleColor = TangemTheme.colors.text.secondary, + showDivider = false, + modifier = Modifier + .background( + color = TangemTheme.colors.background.primary, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .testTag(SendSelectNetworkFeeBottomSheetTestTags.NONCE_INPUT_ITEM), + ) + } + } +} + +@Composable +private fun RegularFeeItemContent( + title: TextReference, + @DrawableRes iconRes: Int, + iconBackgroundColor: Color, + iconTint: Color, + modifier: Modifier = Modifier, + preDot: TextReference? = null, + postDot: TextReference? = null, + ellipsizeOffset: Int? = null, +) { + Column(modifier = modifier) { + Row( + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 15.dp) + .testTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_FEE_ITEM), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier + .size(36.dp) + .background(color = iconBackgroundColor, shape = CircleShape) + .padding(6.dp) + .testTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_ICON), + painter = painterResource(iconRes), + tint = iconTint, + contentDescription = null, + ) + FeeDescription( + title = title, + preDot = preDot, + postDot = postDot, + ellipsizeOffset = ellipsizeOffset, + ) + } + } +} + +@Composable +private fun FeeDescription( + title: TextReference, + modifier: Modifier = Modifier, + preDot: TextReference? = null, + postDot: TextReference? = null, + ellipsizeOffset: Int? = null, +) { + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = title.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_TITLE), + ) + if (preDot != null) { + FeeValueContent(preDot = preDot, postDot = postDot, ellipsizeOffset = ellipsizeOffset) + } + } +} + +@Composable +private fun FeeValueContent(preDot: TextReference, postDot: TextReference?, ellipsizeOffset: Int? = null) { + val ellipsis = if (ellipsizeOffset == null) { + TextEllipsis.End + } else { + TextEllipsis.OffsetEnd(ellipsizeOffset) + } + val textColor = TangemTheme.colors.text.tertiary + val textStyle = TangemTheme.typography.caption1 + Row { + EllipsisText( + text = preDot.resolveReference(), + style = textStyle, + color = textColor, + textAlign = TextAlign.End, + ellipsis = ellipsis, + modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.TOKEN_AMOUNT), + ) + if (postDot != null) { + Text( + text = StringsSigns.DOT, + style = textStyle, + color = textColor, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing4) + .testTag(SendSelectNetworkFeeBottomSheetTestTags.DOT_SIGN), + ) + Text( + text = postDot.resolveReference(), + style = textStyle, + color = textColor, + modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.FIAT_AMOUNT), + ) + } + } +} + +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview( + @PreviewParameter(FeeSpeedSelectorUMContentProvider::class) + state: FeeSelectorUM.Content, +) { + TangemThemePreview { + Box(Modifier.background(TangemTheme.colors.background.secondary)) { + FeeSpeedSelectorContent( + state = state, + intents = StubFeeSpeedSelectorIntents(), + ) + } + } +} + +private class FeeSpeedSelectorUMContentProvider : CollectionPreviewParameterProvider( + collection = listOf( + FeeSelectorUM.Content( + isPrimaryButtonEnabled = false, + feeItems = persistentListOf( + FeeItem.Suggested( + title = resourceReference( + id = R.string.wc_fee_suggested, + formatArgs = wrappedList("Tangem"), + ), + fee = Fee.Common(Amount(value = BigDecimal("0.1"), blockchain = Blockchain.Ethereum)), + ), + FeeItem.Slow(fee = Fee.Common(Amount(value = BigDecimal("0.01"), blockchain = Blockchain.Ethereum))), + FeeItem.Market( + fee = Fee.Common( + Amount( + value = BigDecimal("0.02"), + blockchain = Blockchain.Ethereum, + ), + ), + ), + FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))), + customFeeItem, + ), + selectedFeeItem = FeeItem.Slow( + fee = Fee.Common( + Amount( + value = BigDecimal("0.01"), + blockchain = Blockchain.Ethereum, + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = true, + isFeeConvertibleToFiat = true, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal.TEN, + appCurrency = AppCurrency.Default, + ), + feeNonce = FeeNonce.None, + fees = TransactionFee.Single(customFeeItem.fee), + ), + ), +) + +private val customFeeItem = FeeItem.Custom( + fee = Fee.Common(Amount(value = BigDecimal("0.05"), blockchain = Blockchain.Ethereum)), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.119806", + onValueChange = {}, + keyboardOptions = KeyboardOptions(), + keyboardActions = KeyboardActions(), + symbol = "ETH", + decimals = 8, + title = resourceReference(R.string.send_max_fee), + footer = resourceReference(R.string.send_custom_amount_fee_footer), + label = stringReference("~ 0,03 \$"), + isReadonly = false, + ), + CustomFeeFieldUM( + value = "40000", + onValueChange = {}, + keyboardOptions = KeyboardOptions(), + keyboardActions = KeyboardActions(), + symbol = "GWEI", + decimals = 8, + title = resourceReference(R.string.send_gas_price), + footer = resourceReference(R.string.send_gas_price_footer), + label = null, + isReadonly = false, + ), + CustomFeeFieldUM( + value = "31400", + onValueChange = {}, + keyboardOptions = KeyboardOptions(), + keyboardActions = KeyboardActions(), + symbol = null, + decimals = 8, + title = resourceReference(R.string.send_gas_limit), + footer = resourceReference(R.string.send_gas_limit_footer), + label = null, + isReadonly = false, + ), + ), +) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt index 9afa349960..295b4f178e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt @@ -2,6 +2,8 @@ package com.tangem.features.send.v2.feeselector.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model +import com.tangem.features.send.v2.feeselector.model.FeeSelectorBlockModel +import com.tangem.features.send.v2.feeselector.component.speed.model.FeeSpeedSelectorModel import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel import dagger.Binds import dagger.Module @@ -16,4 +18,14 @@ internal interface FeeSelectorModelModule { @IntoMap @ClassKey(FeeSelectorModel::class) fun bindModel(model: FeeSelectorModel): Model + + @Binds + @IntoMap + @ClassKey(FeeSelectorBlockModel::class) + fun bindBlockModel(model: FeeSelectorBlockModel): Model + + @Binds + @IntoMap + @ClassKey(FeeSpeedSelectorModel::class) + fun bindFeeSelectorSpeedModel(model: FeeSpeedSelectorModel): Model } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorBlockModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorBlockModel.kt new file mode 100644 index 0000000000..e8f6e511c9 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorBlockModel.kt @@ -0,0 +1,78 @@ +package com.tangem.features.send.v2.feeselector.model + +import androidx.compose.runtime.Stable +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.common.TangemBlogUrlBuilder +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.settings.NeverShowTapHelpUseCase +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource +import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +internal class FeeSelectorBlockModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val feeSelectorLogicFactory: FeeSelectorLogic.Factory, + private val analyticsEventHandler: AnalyticsEventHandler, + private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, + private val urlOpener: UrlOpener, +) : Model(), FeeSelectorModelCallback { + + private val params = paramsContainer.require() + private val feeSelectorLogic = feeSelectorLogicFactory.create( + params = params, + modelScope = modelScope, + ) + + val feeSelectorBottomSheet = SlotNavigation() + val uiState: StateFlow + field = feeSelectorLogic.uiState + + override fun onFeeResult(feeSelectorUM: FeeSelectorUM) { + uiState.value = feeSelectorUM + feeSelectorBottomSheet.dismiss() + } + + fun updateState(feeSelectorUM: FeeSelectorUM) { + uiState.value = feeSelectorUM + } + + fun onReadMoreClicked() { + modelScope.launch { + urlOpener.openUrl(TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.WhatIsTransactionFee)) + } + } + + fun showFeeSelector() { + analyticsEventHandler.send( + CommonSendAnalyticEvents.FeeScreenOpened( + categoryName = params.analyticsCategoryName, + source = params.analyticsSendSource, + ), + ) + analyticsEventHandler.send( + CommonSendAnalyticEvents.ScreenReopened( + categoryName = params.analyticsCategoryName, + source = SendScreenSource.Fee, + ), + ) + modelScope.launch { + neverShowTapHelpUseCase() + feeSelectorBottomSheet.activate(Unit) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt index d35aa6784b..ad255feda4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt @@ -1,7 +1,9 @@ package com.tangem.features.send.v2.feeselector.model +import androidx.compose.runtime.Stable import com.tangem.features.send.v2.api.entity.FeeItem +@Stable internal interface FeeSelectorIntents { fun onFeeItemSelected(feeItem: FeeItem) fun onCustomFeeValueChange(index: Int, value: String) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt new file mode 100644 index 0000000000..fd8f950d3e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt @@ -0,0 +1,202 @@ +package com.tangem.features.send.v2.feeselector.model + +import arrow.core.getOrElse +import com.tangem.blockchain.common.AmountType +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.NonceInserted +import com.tangem.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.api.entity.FeeNonce +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener +import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger +import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadListener +import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents +import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents.GasPriceInserter +import com.tangem.features.send.v2.feeselector.model.transformers.* +import com.tangem.utils.transformer.update +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +@Suppress("LongParameterList") +internal class FeeSelectorLogic @AssistedInject constructor( + @Assisted private val params: FeeSelectorParams, + @Assisted private val modelScope: CoroutineScope, + private val isFeeApproximateUseCase: IsFeeApproximateUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val feeSelectorReloadListener: FeeSelectorReloadListener, + private val feeSelectorCheckReloadListener: FeeSelectorCheckReloadListener, + private val feeSelectorCheckReloadTrigger: FeeSelectorCheckReloadTrigger, + private val feeSelectorAlertFactory: FeeSelectorAlertFactory, + private val analyticsEventHandler: AnalyticsEventHandler, +) : FeeSelectorIntents { + + private var appCurrency: AppCurrency = AppCurrency.Default + val uiState = MutableStateFlow(params.state) + + init { + initAppCurrency() + subscribeOnFeeReloadTriggerUpdates() + subscribeOnFeeCheckReloadTriggerUpdates() + subscribeOnFeeLoadingStateTriggerUpdates() + loadFee() + } + + fun updateState(feeSelectorUM: FeeSelectorUM) { + uiState.value = feeSelectorUM + } + + private fun initAppCurrency() { + modelScope.launch { + appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } + } + } + + private fun loadFee() { + if (uiState.value !is FeeSelectorUM.Content) { + uiState.update(FeeSelectorLoadingTransformer) + } + modelScope.launch { + params.onLoadFee() + .fold( + ifLeft = { error -> uiState.update(FeeSelectorErrorTransformer(error)) }, + ifRight = { fee -> + uiState.update( + FeeSelectorLoadedTransformer( + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + appCurrency = appCurrency, + fees = fee, + feeStateConfiguration = params.feeStateConfiguration, + isFeeApproximate = isFeeApproximate(fee.normal.amount.type), + feeSelectorIntents = this@FeeSelectorLogic, + ), + ) + }, + ) + } + } + + private fun isFeeApproximate(amountType: AmountType): Boolean { + val networkId = params.feeCryptoCurrencyStatus.currency.network.id + return isFeeApproximateUseCase(networkId = networkId, amountType = amountType) + } + + override fun onFeeItemSelected(feeItem: FeeItem) { + if (feeItem is FeeItem.Custom) { + analyticsEventHandler.send( + CommonSendFeeAnalyticEvents.CustomFeeButtonClicked(categoryName = params.analyticsCategoryName), + ) + } + uiState.update(FeeItemSelectedTransformer(feeItem)) + if (feeItem !is FeeItem.Custom) { + onDoneClick() + } + } + + override fun onCustomFeeValueChange(index: Int, value: String) { + uiState.update( + FeeSelectorCustomValueChangedTransformer( + index = index, + value = value, + intents = this, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + ), + ) + } + + override fun onNonceChange(value: String) { + uiState.update(FeeSelectorNonceChangeTransformer(value = value)) + } + + override fun onDoneClick() { + val feeSelectorUM = uiState.value as? FeeSelectorUM.Content ?: return + analyticsEventHandler.send( + CommonSendFeeAnalyticEvents.SelectedFee( + categoryName = params.analyticsCategoryName, + feeType = feeSelectorUM.toAnalyticType(), + source = params.analyticsSendSource, + ), + ) + val isCustomFeeEdited = feeSelectorUM.selectedFeeItem.fee.amount.value != feeSelectorUM.fees.normal.amount.value + if (feeSelectorUM.selectedFeeItem is FeeItem.Custom && isCustomFeeEdited) { + analyticsEventHandler.send(GasPriceInserter(categoryName = params.analyticsCategoryName)) + } + if (feeSelectorUM.feeNonce is FeeNonce.Nonce) { + analyticsEventHandler.send( + NonceInserted( + categoryName = params.analyticsCategoryName, + token = params.feeCryptoCurrencyStatus.currency.symbol, + blockchain = params.feeCryptoCurrencyStatus.currency.network.name, + ), + ) + } + + (params as? FeeSelectorParams.FeeSelectorDetailsParams)?.callback?.onFeeResult(uiState.value) + } + + private fun subscribeOnFeeReloadTriggerUpdates() { + feeSelectorReloadListener.reloadTriggerFlow + .onEach { data -> + if (data.isRemoveSuggestedFee) { + uiState.update(FeeSelectorRemoveSuggestedTransformer) + } + loadFee() + } + .launchIn(modelScope) + } + + private fun subscribeOnFeeLoadingStateTriggerUpdates() { + feeSelectorReloadListener.loadingStateTriggerFlow + .onEach { uiState.update(FeeSelectorLoadingTransformer) } + .launchIn(modelScope) + } + + private fun subscribeOnFeeCheckReloadTriggerUpdates() { + feeSelectorCheckReloadListener.checkReloadTriggerFlow + .onEach { checkLoadFee() } + .launchIn(modelScope) + } + + private fun checkLoadFee() { + modelScope.launch { + params.onLoadFee().fold( + ifRight = { newFee -> + feeSelectorAlertFactory.getFeeUpdatedAlert( + newTransactionFee = newFee, + feeSelectorUM = uiState.value, + proceedAction = { + modelScope.launch { + feeSelectorCheckReloadTrigger.callbackCheckResult(true) + } + }, + stopAction = { + modelScope.launch { + feeSelectorCheckReloadTrigger.callbackCheckResult(false) + } + }, + ) + }, + ifLeft = { feeError -> + feeSelectorCheckReloadTrigger.callbackCheckResult(false) + feeSelectorAlertFactory.getFeeUnreachableErrorState(::loadFee) + }, + ) + } + } + + @AssistedFactory + interface Factory { + fun create(params: FeeSelectorParams, modelScope: CoroutineScope): FeeSelectorLogic + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index c290cc9fb3..706881dbe0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -1,246 +1,43 @@ package com.tangem.features.send.v2.feeselector.model import androidx.compose.runtime.Stable -import arrow.core.getOrElse -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.blockchain.common.AmountType -import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.settings.NeverShowTapHelpUseCase -import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase -import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.NonceInserted -import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource -import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback -import com.tangem.features.send.v2.api.entity.FeeItem -import com.tangem.features.send.v2.api.entity.FeeNonce -import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.params.FeeSelectorParams -import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener -import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger -import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadListener -import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents -import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents.GasPriceInserter -import com.tangem.features.send.v2.feeselector.model.transformers.* -import com.tangem.utils.TangemBlogUrlBuilder +import com.tangem.features.send.v2.feeselector.route.FeeSelectorRoute import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.transformer.update -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import javax.inject.Inject -@Suppress("LongParameterList") @Stable @ModelScoped internal class FeeSelectorModel @Inject constructor( paramsContainer: ParamsContainer, - private val urlOpener: UrlOpener, - private val isFeeApproximateUseCase: IsFeeApproximateUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, - private val feeSelectorReloadListener: FeeSelectorReloadListener, - private val feeSelectorCheckReloadListener: FeeSelectorCheckReloadListener, - private val feeSelectorCheckReloadTrigger: FeeSelectorCheckReloadTrigger, - private val feeSelectorAlertFactory: FeeSelectorAlertFactory, override val dispatchers: CoroutineDispatcherProvider, - private val analyticsEventHandler: AnalyticsEventHandler, -) : Model(), FeeSelectorIntents, FeeSelectorModelCallback { + private val feeSelectorLogicFactory: FeeSelectorLogic.Factory, + private val sendFeatureToggles: SendFeatureToggles, +) : Model() { private val params = paramsContainer.require() - private var appCurrency: AppCurrency = AppCurrency.Default + private val feeSelectorLogic = feeSelectorLogicFactory.create( + params = params, + modelScope = modelScope, + ) - val feeSelectorBottomSheet = SlotNavigation() + val uiState = feeSelectorLogic.uiState + val intents: FeeSelectorIntents = feeSelectorLogic - val uiState: StateFlow - field = MutableStateFlow(params.state) - - init { - initAppCurrency() - subscribeOnFeeReloadTriggerUpdates() - subscribeOnFeeCheckReloadTriggerUpdates() - subscribeOnFeeLoadingStateTriggerUpdates() - loadFee() + fun onChildBack() { + // TODO("Not yet implemented") } - fun updateState(feeSelectorUM: FeeSelectorUM) { - uiState.value = feeSelectorUM - } - - fun onReadMoreClicked() { - urlOpener.openUrl(TangemBlogUrlBuilder.FEE_BLOG_LINK) - } - - private fun initAppCurrency() { - modelScope.launch { - appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } - } - } - - private fun loadFee() { - if (uiState.value !is FeeSelectorUM.Content) { - uiState.update(FeeSelectorLoadingTransformer) - } - modelScope.launch { - params.onLoadFee() - .fold( - ifLeft = { error -> uiState.update(FeeSelectorErrorTransformer(error)) }, - ifRight = { fee -> - uiState.update( - FeeSelectorLoadedTransformer( - cryptoCurrencyStatus = params.cryptoCurrencyStatus, - feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, - appCurrency = appCurrency, - fees = fee, - feeStateConfiguration = params.feeStateConfiguration, - isFeeApproximate = isFeeApproximate(fee.normal.amount.type), - feeSelectorIntents = this@FeeSelectorModel, - ), - ) - }, - ) - } - } - - private fun isFeeApproximate(amountType: AmountType): Boolean { - val networkId = params.feeCryptoCurrencyStatus.currency.network.id - return isFeeApproximateUseCase(networkId = networkId, amountType = amountType) - } - - override fun onFeeItemSelected(feeItem: FeeItem) { - if (feeItem is FeeItem.Custom) { - analyticsEventHandler.send( - CommonSendFeeAnalyticEvents.CustomFeeButtonClicked(categoryName = params.analyticsCategoryName), - ) - } - uiState.update(FeeItemSelectedTransformer(feeItem)) - if (feeItem !is FeeItem.Custom) { - onDoneClick() - } - } - - override fun onCustomFeeValueChange(index: Int, value: String) { - uiState.update( - FeeSelectorCustomValueChangedTransformer( - index = index, - value = value, - intents = this, - appCurrency = appCurrency, - feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, - ), - ) - } - - override fun onNonceChange(value: String) { - uiState.update(FeeSelectorNonceChangeTransformer(value = value)) - } - - override fun onDoneClick() { - val feeSelectorUM = uiState.value as? FeeSelectorUM.Content ?: return - analyticsEventHandler.send( - CommonSendFeeAnalyticEvents.SelectedFee( - categoryName = params.analyticsCategoryName, - feeType = feeSelectorUM.toAnalyticType(), - source = params.analyticsSendSource, - ), - ) - val isCustomFeeEdited = feeSelectorUM.selectedFeeItem.fee.amount.value != feeSelectorUM.fees.normal.amount.value - if (feeSelectorUM.selectedFeeItem is FeeItem.Custom && isCustomFeeEdited) { - analyticsEventHandler.send(GasPriceInserter(categoryName = params.analyticsCategoryName)) - } - if (feeSelectorUM.feeNonce is FeeNonce.Nonce) { - analyticsEventHandler.send( - NonceInserted( - categoryName = params.analyticsCategoryName, - token = params.feeCryptoCurrencyStatus.currency.symbol, - blockchain = params.feeCryptoCurrencyStatus.currency.network.name, - ), - ) + fun getInitialRoute(): FeeSelectorRoute { + if (sendFeatureToggles.isGaslessTransactionsEnabled.not()) { + return FeeSelectorRoute.ChooseSpeed } - (params as? FeeSelectorParams.FeeSelectorDetailsParams)?.callback?.onFeeResult(uiState.value) - } - - override fun onFeeResult(feeSelectorUM: FeeSelectorUM) { - uiState.value = feeSelectorUM - feeSelectorBottomSheet.dismiss() - } - - fun showFeeSelector() { - analyticsEventHandler.send( - CommonSendAnalyticEvents.FeeScreenOpened( - categoryName = params.analyticsCategoryName, - source = params.analyticsSendSource, - ), - ) - analyticsEventHandler.send( - CommonSendAnalyticEvents.ScreenReopened( - categoryName = params.analyticsCategoryName, - source = SendScreenSource.Fee, - ), - ) - modelScope.launch { - neverShowTapHelpUseCase() - feeSelectorBottomSheet.activate(Unit) - } - } - - private fun subscribeOnFeeReloadTriggerUpdates() { - feeSelectorReloadListener.reloadTriggerFlow - .onEach { data -> - if (data.isRemoveSuggestedFee) { - uiState.update(FeeSelectorRemoveSuggestedTransformer) - } - loadFee() - } - .launchIn(modelScope) - } - - private fun subscribeOnFeeLoadingStateTriggerUpdates() { - feeSelectorReloadListener.loadingStateTriggerFlow - .onEach { uiState.update(FeeSelectorLoadingTransformer) } - .launchIn(modelScope) - } - - private fun subscribeOnFeeCheckReloadTriggerUpdates() { - feeSelectorCheckReloadListener.checkReloadTriggerFlow - .onEach { checkLoadFee() } - .launchIn(modelScope) - } - - private fun checkLoadFee() { - modelScope.launch { - params.onLoadFee().fold( - ifRight = { newFee -> - feeSelectorAlertFactory.getFeeUpdatedAlert( - newTransactionFee = newFee, - feeSelectorUM = uiState.value, - proceedAction = { - modelScope.launch { - feeSelectorCheckReloadTrigger.callbackCheckResult(true) - } - }, - stopAction = { - modelScope.launch { - feeSelectorCheckReloadTrigger.callbackCheckResult(false) - } - }, - ) - }, - ifLeft = { feeError -> - feeSelectorCheckReloadTrigger.callbackCheckResult(false) - feeSelectorAlertFactory.getFeeUnreachableErrorState(::loadFee) - }, - ) - } + // TODO: Add logic to choose initial route based on other conditions + return FeeSelectorRoute.ChooseSpeed } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/route/FeeSelectorRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/route/FeeSelectorRoute.kt new file mode 100644 index 0000000000..f122f8fed0 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/route/FeeSelectorRoute.kt @@ -0,0 +1,30 @@ +package com.tangem.features.send.v2.feeselector.route + +import androidx.compose.runtime.Immutable +import com.tangem.core.decompose.navigation.Route +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.send.v2.impl.R +import kotlinx.serialization.Serializable + +@Immutable +@Serializable +internal sealed interface FeeSelectorRoute : Route { + + val title: TextReference + + @Serializable + data object NetworkFee : FeeSelectorRoute { + override val title: TextReference = resourceReference(R.string.common_fee_selector_title) + } + + @Serializable + data object ChooseSpeed : FeeSelectorRoute { + override val title: TextReference = resourceReference(R.string.fee_selector_choose_speed_title) + } + + @Serializable + data object ChooseToken : FeeSelectorRoute { + override val title: TextReference = resourceReference(R.string.fee_selector_choose_token_title) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index b115e87f71..7bf68aab96 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -1,65 +1,35 @@ package com.tangem.features.send.v2.feeselector.ui -import android.content.res.Configuration -import androidx.annotation.DrawableRes -import androidx.compose.animation.* -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.Text +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEachIndexed -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.fade +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.ChildStack import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.atoms.text.EllipsisText -import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter -import com.tangem.core.ui.components.inputrow.InputRowEnter -import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmountV2 -import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.fee -import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.test.SendSelectNetworkFeeBottomSheetTestTags -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.send.v2.api.entity.* +import com.tangem.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents -import com.tangem.features.send.v2.feeselector.model.StubFeeSelectorIntents +import com.tangem.features.send.v2.feeselector.route.FeeSelectorRoute import com.tangem.features.send.v2.impl.R -import com.tangem.utils.StringsSigns -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import java.math.BigDecimal @Composable internal fun FeeSelectorModalBottomSheet( + childStack: ChildStack, state: FeeSelectorUM, feeSelectorIntents: FeeSelectorIntents, feeDisplaySource: FeeSelectorParams.FeeDisplaySource, @@ -73,16 +43,12 @@ internal fun FeeSelectorModalBottomSheet( onDismissRequest = onDismiss, content = TangemBottomSheetConfigContent.Empty, ), - containerColor = TangemTheme.colors.background.primary, + containerColor = TangemTheme.colors.background.tertiary, title = { - FeeTitle(feeDisplaySource = feeDisplaySource, onDismiss = onDismiss) + FeeTitle(childStack = childStack, feeDisplaySource = feeDisplaySource, onDismiss = onDismiss) }, content = { - FeeSelectorItems( - state = state, - feeSelectorIntents = feeSelectorIntents, - modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp), - ) + Content(childStack = childStack) }, footer = if (state.selectedFeeItem is FeeItem.Custom) { { @@ -102,414 +68,46 @@ internal fun FeeSelectorModalBottomSheet( } @Composable -private fun FeeTitle(feeDisplaySource: FeeSelectorParams.FeeDisplaySource, onDismiss: () -> Unit) { - when (feeDisplaySource) { - FeeSelectorParams.FeeDisplaySource.Screen -> { - TangemModalBottomSheetTitle( - title = resourceReference(R.string.common_network_fee_title), - endIconRes = R.drawable.ic_close_24, - onEndClick = onDismiss, - ) - } - FeeSelectorParams.FeeDisplaySource.BottomSheet -> { - TangemModalBottomSheetTitle( - title = resourceReference(R.string.common_network_fee_title), - startIconRes = R.drawable.ic_back_24, - onStartClick = onDismiss, - ) - } - } -} - -@Composable -private fun FeeSelectorItems( - state: FeeSelectorUM.Content, - feeSelectorIntents: FeeSelectorIntents, - modifier: Modifier = Modifier, +private fun FeeTitle( + childStack: ChildStack, + feeDisplaySource: FeeSelectorParams.FeeDisplaySource, + onDismiss: () -> Unit, ) { - Column(modifier = modifier) { - val feeFiatRateUM = state.feeFiatRateUM - state.feeItems.fastForEachIndexed { index, item -> - val isSelected = item.isSameClass(state.selectedFeeItem) - val isLastItem = index == state.feeItems.size - 1 - val iconTint by animateColorAsState( - targetValue = if (isSelected) TangemTheme.colors.icon.accent else TangemTheme.colors.text.tertiary, - label = "Fee selector icon tint change", - ) - val iconBackgroundColor by animateColorAsState( - targetValue = if (isSelected) { - TangemTheme.colors.icon.accent.copy(alpha = 0.1F) - } else { - TangemTheme.colors.background.secondary - }, - label = "Fee selector icon background change", - ) - val itemModifier = Modifier - .fillMaxWidth() - .selectedBorder(isSelected = isSelected) - .clickableSingle(onClick = { feeSelectorIntents.onFeeItemSelected(item) }) - when (item) { - is FeeItem.Custom -> CustomFeeBlock( - modifier = itemModifier, - customFee = item, - isSelected = isSelected, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - onValueChange = feeSelectorIntents::onCustomFeeValueChange, - nonce = state.feeNonce, + Children( + stack = childStack, + animation = stackAnimation(fade(animationSpec = tween(durationMillis = 100))), + ) { current -> + when (feeDisplaySource) { + FeeSelectorParams.FeeDisplaySource.Screen -> { + TangemModalBottomSheetTitle( + title = current.configuration.title, + endIconRes = R.drawable.ic_close_24, + onEndClick = onDismiss, ) - else -> RegularFeeItemContent( - modifier = itemModifier, - title = item.title, - iconRes = item.iconRes, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (state.feeExtraInfo.isFeeConvertibleToFiat && feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !isLastItem, + } + FeeSelectorParams.FeeDisplaySource.BottomSheet -> { + TangemModalBottomSheetTitle( + title = current.configuration.title, + startIconRes = R.drawable.ic_back_24, + onStartClick = onDismiss, ) } } } } -@Suppress("LongParameterList") @Composable -private fun CustomFeeBlock( - customFee: FeeItem.Custom, - isSelected: Boolean, - iconBackgroundColor: Color, - iconTint: Color, - onValueChange: (Int, String) -> Unit, - nonce: FeeNonce, +private fun Content( + childStack: ChildStack, modifier: Modifier = Modifier, ) { - Column(modifier = modifier) { - Row( - modifier = Modifier - .padding(all = 12.dp) - .testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_FEE_ITEM), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - modifier = Modifier - .size(36.dp) - .background(color = iconBackgroundColor, shape = CircleShape) - .padding(6.dp) - .testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_ITEM_ICON), - painter = painterResource(R.drawable.ic_edit_v2_24), - tint = iconTint, - contentDescription = null, - ) - Text( - text = stringResourceSafe(R.string.common_custom), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle2, - modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_ITEM_TITLE), - ) - } - AnimatedVisibility( - visible = isSelected, - label = "Custom Fee Selected Animation", - enter = expandVertically().plus(fadeIn()), - exit = shrinkVertically().plus(fadeOut()), - ) { - ExpandedCustomFeeItems( - customFeeFields = customFee.customValues, - onValueChange = onValueChange, - nonce = nonce, - ) - } + Children( + stack = childStack, + animation = stackAnimation(fade(animationSpec = tween(durationMillis = 100))), + modifier = modifier + .fillMaxSize() + .animateContentSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) } -} - -@Composable -private fun ExpandedCustomFeeItems( - customFeeFields: ImmutableList, - onValueChange: (Int, String) -> Unit, - nonce: FeeNonce, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier) { - customFeeFields.fastForEachIndexed { index, field -> - val isShowDivider = index != customFeeFields.size - 1 || nonce is FeeNonce.Nonce - if (field.label != null) { - InputRowEnterInfoAmountV2( - text = field.value, - decimals = field.decimals, - symbol = field.symbol, - title = field.title, - titleColor = TangemTheme.colors.text.tertiary, - info = field.label, - description = field.footer, - keyboardOptions = field.keyboardOptions, - keyboardActions = field.keyboardActions, - onValueChange = { onValueChange(index, it) }, - showDivider = isShowDivider, - isReadOnly = field.isReadonly, - ) - } else { - InputRowEnterInfoAmountV2( - text = field.value, - decimals = field.decimals, - title = field.title, - titleColor = TangemTheme.colors.text.tertiary, - symbol = field.symbol, - description = field.footer, - onValueChange = { onValueChange(index, it) }, - keyboardOptions = field.keyboardOptions, - keyboardActions = field.keyboardActions, - showDivider = isShowDivider, - ) - } - } - - if (nonce is FeeNonce.Nonce) { - InputRowEnter( - text = nonce.nonce?.toString().orEmpty(), - title = resourceReference(R.string.send_nonce), - description = resourceReference(R.string.send_nonce_footer), - onValueChange = nonce.onNonceChange, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - placeholder = resourceReference(R.string.send_nonce_hint), - titleColor = TangemTheme.colors.text.secondary, - showDivider = false, - modifier = Modifier - .background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .testTag(SendSelectNetworkFeeBottomSheetTestTags.NONCE_INPUT_ITEM), - ) - } - } -} - -@Composable -private fun RegularFeeItemContent( - title: TextReference, - @DrawableRes iconRes: Int, - iconBackgroundColor: Color, - iconTint: Color, - modifier: Modifier = Modifier, - preDot: TextReference? = null, - postDot: TextReference? = null, - ellipsizeOffset: Int? = null, - showDivider: Boolean = true, -) { - Column(modifier = modifier) { - Row( - modifier = Modifier - .padding(all = 12.dp) - .testTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_FEE_ITEM), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - modifier = Modifier - .size(36.dp) - .background(color = iconBackgroundColor, shape = CircleShape) - .padding(6.dp) - .testTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_ICON), - painter = painterResource(iconRes), - tint = iconTint, - contentDescription = null, - ) - FeeDescription( - title = title, - preDot = preDot, - postDot = postDot, - ellipsizeOffset = ellipsizeOffset, - ) - } - if (showDivider) { - HorizontalDivider( - modifier = Modifier.padding(start = 60.dp, end = 12.dp), - color = TangemTheme.colors.stroke.primary, - thickness = 0.5.dp, - ) - } - } -} - -@Composable -private fun FeeDescription( - title: TextReference, - modifier: Modifier = Modifier, - preDot: TextReference? = null, - postDot: TextReference? = null, - ellipsizeOffset: Int? = null, -) { - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text( - text = title.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle2, - modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_TITLE), - ) - if (preDot != null) { - FeeValueContent(preDot = preDot, postDot = postDot, ellipsizeOffset = ellipsizeOffset) - } - } -} - -@Composable -private fun FeeValueContent(preDot: TextReference, postDot: TextReference?, ellipsizeOffset: Int? = null) { - val ellipsis = if (ellipsizeOffset == null) { - TextEllipsis.End - } else { - TextEllipsis.OffsetEnd(ellipsizeOffset) - } - val textColor = TangemTheme.colors.text.tertiary - val textStyle = TangemTheme.typography.caption1 - Row { - EllipsisText( - text = preDot.resolveReference(), - style = textStyle, - color = textColor, - textAlign = TextAlign.End, - ellipsis = ellipsis, - modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.TOKEN_AMOUNT), - ) - if (postDot != null) { - Text( - text = StringsSigns.DOT, - style = textStyle, - color = textColor, - textAlign = TextAlign.Center, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing4) - .testTag(SendSelectNetworkFeeBottomSheetTestTags.DOT_SIGN), - ) - Text( - text = postDot.resolveReference(), - style = textStyle, - color = textColor, - modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.FIAT_AMOUNT), - ) - } - } -} - -@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) -@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun FeeSelectorBS_Preview( - @PreviewParameter(FeeSelectorUMContentProvider::class) - state: FeeSelectorUM.Content, -) { - TangemThemePreview { - FeeSelectorModalBottomSheet( - onDismiss = {}, - state = state, - feeSelectorIntents = StubFeeSelectorIntents(), - feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet, - ) - } -} - -private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider( - collection = listOf( - FeeSelectorUM.Content( - isPrimaryButtonEnabled = false, - feeItems = persistentListOf( - FeeItem.Suggested( - title = resourceReference( - id = R.string.wc_fee_suggested, - formatArgs = wrappedList("Tangem"), - ), - fee = Fee.Common(Amount(value = BigDecimal("0.1"), blockchain = Blockchain.Ethereum)), - ), - FeeItem.Slow(fee = Fee.Common(Amount(value = BigDecimal("0.01"), blockchain = Blockchain.Ethereum))), - FeeItem.Market( - fee = Fee.Common( - Amount( - value = BigDecimal("0.02"), - blockchain = Blockchain.Ethereum, - ), - ), - ), - FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))), - customFeeItem, - ), - selectedFeeItem = FeeItem.Slow( - fee = Fee.Common( - Amount( - value = BigDecimal("0.01"), - blockchain = Blockchain.Ethereum, - ), - ), - ), - feeExtraInfo = FeeExtraInfo( - isFeeApproximate = true, - isFeeConvertibleToFiat = true, - isTronToken = false, - ), - feeFiatRateUM = FeeFiatRateUM( - rate = BigDecimal.TEN, - appCurrency = AppCurrency.Default, - ), - feeNonce = FeeNonce.None, - fees = TransactionFee.Single(customFeeItem.fee), - ), - ), -) - -private val customFeeItem = FeeItem.Custom( - fee = Fee.Common(Amount(value = BigDecimal("0.05"), blockchain = Blockchain.Ethereum)), - customValues = persistentListOf( - CustomFeeFieldUM( - value = "0.119806", - onValueChange = {}, - keyboardOptions = KeyboardOptions(), - keyboardActions = KeyboardActions(), - symbol = "ETH", - decimals = 8, - title = resourceReference(R.string.send_max_fee), - footer = resourceReference(R.string.send_custom_amount_fee_footer), - label = stringReference("~ 0,03 \$"), - isReadonly = false, - ), - CustomFeeFieldUM( - value = "40000", - onValueChange = {}, - keyboardOptions = KeyboardOptions(), - keyboardActions = KeyboardActions(), - symbol = "GWEI", - decimals = 8, - title = resourceReference(R.string.send_gas_price), - footer = resourceReference(R.string.send_gas_price_footer), - label = null, - isReadonly = false, - ), - CustomFeeFieldUM( - value = "31400", - onValueChange = {}, - keyboardOptions = KeyboardOptions(), - keyboardActions = KeyboardActions(), - symbol = null, - decimals = 8, - title = resourceReference(R.string.send_gas_limit), - footer = resourceReference(R.string.send_gas_limit_footer), - label = null, - isReadonly = false, - ), - ), -) \ No newline at end of file +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt index c17c778409..fe864a450f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt @@ -12,6 +12,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.audits.AuditLabel +import com.tangem.core.ui.components.audits.AuditLabelUM +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.v2.api.FeeSelectorBlockComponent @@ -52,6 +55,12 @@ internal class PreviewFeeSelectorBlockComponent : FeeSelectorBlockComponent { tint = TangemTheme.colors.icon.informative, ) Spacer(modifier = Modifier.weight(1f)) + AuditLabel( + state = AuditLabelUM( + text = stringReference("ETH"), + type = AuditLabelUM.Type.General, + ), + ) Text( text = "~ 0.25 \$", style = TangemTheme.typography.body1,