From 6c8865caae6bf00b24ac30be197053fa8238824b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Mar 2024 12:48:57 +0100 Subject: [PATCH 001/109] Updated on 2026-08-14 --- .../send/ui/dialogs/KaspaWarningDialog.kt | 8 +- app/src/main/res/layout/fragment_shop.xml | 2 +- core/res/src/main/res/values-de/strings.xml | 138 +- core/res/src/main/res/values-fr/strings.xml | 138 +- core/res/src/main/res/values-it/strings.xml | 138 +- .../main/res/values-ru/strings-blockchain.xml | 2 +- core/res/src/main/res/values-ru/strings.xml | 1518 ++++++++-------- .../res/values-zh-rTW/strings-blockchain.xml | 7 +- .../src/main/res/values-zh-rTW/strings.xml | 916 +++++----- .../main/res/values/strings-blockchain.xml | 3 +- core/res/src/main/res/values/strings.xml | 1536 +++++++++-------- .../presentation/state/SendNotification.kt | 2 +- .../components/TokenDetailsNotification.kt | 2 +- 13 files changed, 2223 insertions(+), 2187 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt index 5e9f50747f..a86e233c8b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt @@ -2,17 +2,20 @@ package com.tangem.tap.features.send.ui.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.tangem.blockchain.common.Blockchain import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.store import com.tangem.wallet.R object KaspaWarningDialog { + fun create(context: Context, dialog: SendAction.Dialog.KaspaWarningDialog): AlertDialog { return AlertDialog.Builder(context).apply { setTitle(R.string.common_warning) setMessage( context.getString( - R.string.kaspa_withdrawal_message_warning, + R.string.common_utxo_validate_withdrawal_message_warning, + Blockchain.Kaspa.fullName, dialog.maxOutputs, dialog.maxAmount.toPlainString(), ), @@ -23,7 +26,6 @@ object KaspaWarningDialog { setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) } - } - .create() + }.create() } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_shop.xml b/app/src/main/res/layout/fragment_shop.xml index 48384d34c6..35df4922f4 100644 --- a/app/src/main/res/layout/fragment_shop.xml +++ b/app/src/main/res/layout/fragment_shop.xml @@ -218,7 +218,7 @@ android:layout_height="wrap_content" android:paddingStart="12dp" android:paddingEnd="12dp" - android:text="@string/shop_total" + android:text="remove AND-6253" android:textColor="@color/textBlack" android:textSize="15sp" app:layout_constraintBottom_toBottomOf="parent" diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index aaccaf5396..aee0c9f45e 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,72 +1,72 @@ - Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet - Tangem Bot - Akzeptieren - Bilanz: %s - Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an - Abbrechen - Entfernen - Erledigt - Fehler - OK - Änderungen speichern - Absenden - Erfolg - Gebühr - Zugangscode - Sie müssen den richtigen Zugangscode eingeben, bevor Sie die Karte scannen. - Langes Tippen - Dieser Mechanismus schützt vor Annäherungsangriffen auf eine Karte. Es wird eine Verzögerung zwischen dem Empfang und der Ausführung eines Befehls erzwungen. Nach der ersten signierten Transaktion wird dieses Telefon mit der Karte verknüpft und die Transaktionen werden sofort signiert - Passcode - Bevor Sie einen Befehl ausführen, der eine Änderung des Kartenstatus zur Folge hat, müssen Sie den Passcode eingeben. - %s Hasch - KartenID - App Währung - Emittent - Signiert - Details - Nutzungsbedingungen - Karte scannen - Tippen Sie um den Zugangscode zu ändern - Tippen Sie um den Passcode zu ändern - Legen Sie die Karte zum Scannen an - Tippen um zu signieren - Legen Sie die Karte an - Der Betrag enthält nicht einige Ihrer Mittel - Ein wallet erstellen - Betrag - Adresse - Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein - Tag - Memo - inkl. Gebühr - Niedrig - Normal - Priorität - Höchstbetrag - Netzgebühr - Gesamt - %1$s und %2$s werden gesendet - ≈ %1$s (inkl. Gebühr: %2$s) - %s wird gesendet - Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert - Ungültige Adresse - Tangem Wallet - Tangem Twin - Die Bilanz wird aufgeladen… - Die Transaktion läuft… - Verifizierte Bilanz - WalletConnect - Das Konto ist nicht erstellt - Diese Karte wird nicht unterstützt - Ihre Tangem-Karte wurde für die Arbeit mit einer anderen App ausgestellt. Bitte sehen Sie sich den Namen und die Anleitung auf Ihrer Karte an und installieren Sie eine korrekte App - Die Adresse wurde erfolgreich kopiert - Keine Internetverbindung - Erhalten - von %s - Senden - an %s - Tangem - OK, ich hab\'s! + Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet + Tangem Bot + Akzeptieren + Bilanz: %s + Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an + Abbrechen + Entfernen + Erledigt + Fehler + Gebühr + Netzgebühr + OK + Änderungen speichern + Absenden + Erfolg + Zugangscode + Sie müssen den richtigen Zugangscode eingeben, bevor Sie die Karte scannen. + Langes Tippen + Dieser Mechanismus schützt vor Annäherungsangriffen auf eine Karte. Es wird eine Verzögerung zwischen dem Empfang und der Ausführung eines Befehls erzwungen. Nach der ersten signierten Transaktion wird dieses Telefon mit der Karte verknüpft und die Transaktionen werden sofort signiert + Passcode + Bevor Sie einen Befehl ausführen, der eine Änderung des Kartenstatus zur Folge hat, müssen Sie den Passcode eingeben. + %s Hasch + KartenID + App Währung + Emittent + Signiert + Details + Nutzungsbedingungen + Karte scannen + Tippen Sie um den Zugangscode zu ändern + Tippen Sie um den Passcode zu ändern + Legen Sie die Karte zum Scannen an + Tippen um zu signieren + Legen Sie die Karte an + Der Betrag enthält nicht einige Ihrer Mittel + Ein wallet erstellen + Betrag + Adresse + Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein + Tag + Memo + inkl. Gebühr + Niedrig + Normal + Priorität + Höchstbetrag + Gesamt + %1$s und %2$s werden gesendet + ≈ %1$s (inkl. Gebühr: %2$s) + %s wird gesendet + Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert + Ungültige Adresse + Tangem Wallet + Tangem Twin + Die Bilanz wird aufgeladen… + Die Transaktion läuft… + Verifizierte Bilanz + WalletConnect + Das Konto ist nicht erstellt + Diese Karte wird nicht unterstützt + Ihre Tangem-Karte wurde für die Arbeit mit einer anderen App ausgestellt. Bitte sehen Sie sich den Namen und die Anleitung auf Ihrer Karte an und installieren Sie eine korrekte App + Die Adresse wurde erfolgreich kopiert + Keine Internetverbindung + Erhalten + von %s + Senden + an %s + Tangem + OK, ich hab\'s! diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 680543d79c..bceed42d51 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1,72 +1,72 @@ - Cette carte n\'est pas conçue pour fonctionner avec Tangem - Tangem Bot - J\'accepte - Solde : %s - Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité - Annuler - Supprimer - Exécuté - Erreur - OK - Sauvegarder les modifications - Envoyer - Avec succès - Commissions - Code d\'accès - Vous devrez entrer le mot de passe correct avant de scanner la carte - Tenez la carte fermement - Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement. - Mot de passe - Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe. - %s hashes - ID de la carte - Monnaie de l\'application - Emetteur - Signé - Référénces - Conditions d\'utilisation - Scannez la carte - Touchez, pour modifier le code d\'accès - Touchez, pour modifier le mot de passe - Posez pour scanner - Touchez pour signer - Posez la carte - Le montant n\'inclut pas certains de vos fonds - Créer un portefeuille - Somme - Adresse - L\'adresse est la même que celle de votre portefeuille - Tag - Memo - Inclure les commissions - Bas - Normal - Priorité - Somme maximale - Commissions du réseau - Total - Sera envoyé %1$s et %2$s - ≈ %1$s (incl. les commissions : %2$s) - Sera envoyé %s - La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps - Adresse incorrecte - Tangem Wallet - Tangem Twin - Solde est en cours de téléchargement… - Transaction en cours… - Solde confirmé - WalletConnect - Compte n\'est pas créé - Cette carte n\'est pas prise en charge - Votre carte Tangem a été créée pour fonctionner avec une autre application. Regardez le nom et les instructions sur votre carte et installez l\'application pertinente - L\'adresse a été copiée avec succès - Pas de connexion internet - Réception - de %s - Envoi - jusqu\'à %s - Tangem - Ok, je l\'ai! + Cette carte n\'est pas conçue pour fonctionner avec Tangem + Tangem Bot + J\'accepte + Solde : %s + Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité + Annuler + Supprimer + Exécuté + Erreur + Commissions + Commissions du réseau + OK + Sauvegarder les modifications + Envoyer + Avec succès + Code d\'accès + Vous devrez entrer le mot de passe correct avant de scanner la carte + Tenez la carte fermement + Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement. + Mot de passe + Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe. + %s hashes + ID de la carte + Monnaie de l\'application + Emetteur + Signé + Référénces + Conditions d\'utilisation + Scannez la carte + Touchez, pour modifier le code d\'accès + Touchez, pour modifier le mot de passe + Posez pour scanner + Touchez pour signer + Posez la carte + Le montant n\'inclut pas certains de vos fonds + Créer un portefeuille + Somme + Adresse + L\'adresse est la même que celle de votre portefeuille + Tag + Memo + Inclure les commissions + Bas + Normal + Priorité + Somme maximale + Total + Sera envoyé %1$s et %2$s + ≈ %1$s (incl. les commissions : %2$s) + Sera envoyé %s + La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps + Adresse incorrecte + Tangem Wallet + Tangem Twin + Solde est en cours de téléchargement… + Transaction en cours… + Solde confirmé + WalletConnect + Compte n\'est pas créé + Cette carte n\'est pas prise en charge + Votre carte Tangem a été créée pour fonctionner avec une autre application. Regardez le nom et les instructions sur votre carte et installez l\'application pertinente + L\'adresse a été copiée avec succès + Pas de connexion internet + Réception + de %s + Envoi + jusqu\'à %s + Tangem + Ok, je l\'ai! diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 6ff58173b7..353953eb8e 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -1,72 +1,72 @@ - Questa carta non è progettata per funzionare con Tangem - Tangem Bot - Accetta - Saldo: %s - Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy - Annulla - Rimuovere - Fatto - Errore - OK - Mantieni le modifiche - Invia - Con successo - Commissione - Codice di accesso - Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto - Mantenimento della carta - Questo meccanismo protegge dagli avvicinamenti senza contatto sulla carta. Attiva un ritardo tra la ricezione e l\'esecuzione di un comando. Dopo la prima transazione firmata, questo telefono verrà associato alla carta e le transazioni verranno firmate immediatamente. - Password - Dovrai inserire una password prima di eseguire qualsiasi comando che modifichi lo stato della carta. - Hash %s - ID carta - Valuta dell\'applicazione - Emittente - Firmato - Requisiti - Termini del servizio - Scansiona carta - Avvicina per modificare il codice di accesso - Avvicina per modificare la password - Avvicina per scansionare - Avvicina per firmare - Avvicina la carta - L\'importo non include alcuni dei tuoi fondi - Crea portafoglio - Importo - Indirizzo - L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio - Tag - Memo - Includi commissione - Insufficiente - Normale - Prioritario - Importo totale - Costi della rete - Totale - Sarà inviato %1$s e %2$s - ≈ %1$s (inc. commissione: %2$s) - Sarà inviato %s - La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo - Indirizzo non valido - Tangem Wallet - Tangem Twin - Il saldo sta per essere caricato… - Transazione in corso… - Saldo verificato - WalletConnect - Conto non creato - Questa carta non è supportata - La tua carta Tangem è stata creata per funzionare con un\'altra applicazione. Leggere il nome e le istruzioni sulla carta e installare l\'applicazione corretta - L\'indirizzo è stato copiato con successo - Nessuna connessione a Internet - Riceviu - da %s - Inviau - su %s - Tangem - Ok, ho capito! + Questa carta non è progettata per funzionare con Tangem + Tangem Bot + Accetta + Saldo: %s + Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy + Annulla + Rimuovere + Fatto + Errore + Commissione + Costi della rete + OK + Mantieni le modifiche + Invia + Con successo + Codice di accesso + Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto + Mantenimento della carta + Questo meccanismo protegge dagli avvicinamenti senza contatto sulla carta. Attiva un ritardo tra la ricezione e l\'esecuzione di un comando. Dopo la prima transazione firmata, questo telefono verrà associato alla carta e le transazioni verranno firmate immediatamente. + Password + Dovrai inserire una password prima di eseguire qualsiasi comando che modifichi lo stato della carta. + Hash %s + ID carta + Valuta dell\'applicazione + Emittente + Firmato + Requisiti + Termini del servizio + Scansiona carta + Avvicina per modificare il codice di accesso + Avvicina per modificare la password + Avvicina per scansionare + Avvicina per firmare + Avvicina la carta + L\'importo non include alcuni dei tuoi fondi + Crea portafoglio + Importo + Indirizzo + L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio + Tag + Memo + Includi commissione + Insufficiente + Normale + Prioritario + Importo totale + Totale + Sarà inviato %1$s e %2$s + ≈ %1$s (inc. commissione: %2$s) + Sarà inviato %s + La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo + Indirizzo non valido + Tangem Wallet + Tangem Twin + Il saldo sta per essere caricato… + Transazione in corso… + Saldo verificato + WalletConnect + Conto non creato + Questa carta non è supportata + La tua carta Tangem è stata creata per funzionare con un\'altra applicazione. Leggere il nome e le istruzioni sulla carta e installare l\'applicazione corretta + L\'indirizzo è stato copiato con successo + Nessuna connessione a Internet + Riceviu + da %s + Inviau + su %s + Tangem + Ok, ho capito! diff --git a/core/res/src/main/res/values-ru/strings-blockchain.xml b/core/res/src/main/res/values-ru/strings-blockchain.xml index c58315db3f..f589ed1f9f 100644 --- a/core/res/src/main/res/values-ru/strings-blockchain.xml +++ b/core/res/src/main/res/values-ru/strings-blockchain.xml @@ -6,9 +6,9 @@ Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт. Произошла ошибка. Код: %s. - Для создания аккаунта отправьте средства на этот адрес Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. + Для создания аккаунта отправьте средства на этот адрес Минимальная сумма: %s Сдача слишком мала Неверная комиссия diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index ea038372cd..d0e06ddf85 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1,753 +1,771 @@ - Добавить токен - Валюты - Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. - Спасибо за ваш отзыв - Отправлено успешно - Обратиться в поддержку - Отправить отзыв - Эта функция недоступна в демонстрационном режиме - Не удалось отправить письмо - Причина: %s - Не могу отправить транзакцию - Выбранный кошелёк не поддерживает сеть %1$s - Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен. - Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки. - У вас возникли трудности со сканированием карты? - Эта карта не предназначена для работы с этим приложением - Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem - Включите биометрическую аутентификацию - Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком. - При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены из приложения. - Сохранение кода доступа - Подключите функцию хранения кодов доступа от карт на телефоне в зашифрованном виде, и при работе с картой вместо кода доступа будет запрашиваться биометрическая аутентификация. - Cохранение кошелька - Подключите функцию привязки карты в приложении, а также возможность биометрической аутентификации. Подпись транзакции все так же потребует карту. - Тёмная - Светлая - Как в системе - Тема - Настройки приложения - Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\" - Больше не показывать - Понятно - Балансы скрыты - Пожалуйста, отсканируйте карту - Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту - Слишком много попыток - Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона. - Начать резервное копирование - - %d карта - %d карты - %d карт - %d карт - - Отключите эту опцию, если не хотите, чтобы эта карта использовалась для сброса кодов доступа на другие карты этого кошелька. Обратите внимание, сброс кода также не будет доступен на этой карте. - Использовать эту карту для сброса кода доступа на других картах в этом кошельке - Отключить возможность сброса кода доступа на этой карте или других картах этого кошелька - Восстановление кода доступа - Сбросить - Вы уверены, что хотите это сделать? - Смена кода доступа - Код доступа будет изменен только на данной карте - Заводские настройки - Тип безопасности - Выбранный способ защиты приложения - Настройки карты - Tangem Bot - Чат - Отправить логи - Принять - Доступ запрещен - Добавить - Применить - Одобрение - Внимание - Баланс: %s - Баланс - биометрическую аутентификацию - биометрией - Купить - Перейти на %1$s - Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности. - Отмена - Закрыть - Продолжить - Копировать - Скопировать адрес - Создать - Удалить - Отключено - Не нравится - Готово - Включить - Включено - Ошибка - Обменять - Обозреватель - Посмотреть историю - Посмотреть историю транзакций - Обозреватель - Комиссия - Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s - Свое - Быстро - По рынку - Медленно - Скорость и комиссия - Получить адреса - К провайдеру - Импортировать - Нравится - Заблокирован - Основная сеть - Далее - Нет - Нет адреса - Нет данных - OK - Основная карта - Вставить - Подробнее - Получить - Отклонить - Перезагрузить - Переименовать - Сохранить изменения - Искать - Поиск токенов - Seed-фраза - Выберите действие - Продать - Отправить - Сервер недоступен, повторите попытку позднее - Поделиться - Подписать - Подписать и отправить - Стейкинг - Начать - Отправить - Успешно - Поддержка - Обмен - условия участия - Ошибка транзакции - Транзакции - Перевод - Я понял - Недоступно - Произошла ошибка. Пожалуйста, попробуйте снова. - Да - Адрес контракта скопирован! - Доступные сети - Добавить токен - Адрес контракта - Адрес контракта некорректен - Путь деривации некорректен - Пожалуйста, выберите сеть - Обязательное поле - Количество знаков после запятой должно быть корректным числом не больше %d - Своя деривация - Например m/00\'/0000\'/0\'/0/0 - Введите свою деривацию - Знаков после запятой - Путь деривации - По умолчанию - Деривация по BIP44 - Введенный путь деривации некорректен - Например, USD Coin - Название токена - Не выбрано - Сеть - Сеть - Вы можете добавить токен в ручную, если он не поддерживается Tangem - Например, USDC - Символ - Символ токена - Этот токен/сеть уже находится в вашем списке - Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить - Остерегайтесь мошеннических токенов, они могут ничего не стоить - Токены могут быть созданы кем угодно - Чат - Код доступа - Перед сканированием карты вам нужно будет ввести правильный код доступа. - Задержка сканирования - Этот механизм защищает карту от бесконтактных атак. Между сканированием карты и выполнением команды будет добавлена задержка. - Пароль - Перед выполнением любой команды, влекущей за собой изменение состояния карты, вам необходимо будет ввести пароль. - Реферальная программа - Переверните экран вашего устройства вниз, чтобы быстро скрыть и отобразить балансы - Privacy policy - %s хэшей - Номер карты - Обратиться в поддержку - Добавить еще карты - Валюта приложения - Скрывать балансы жестом переворота - Эмитент - Подписано - Подробности - Проверьте подключение с интернетом или переключитесь на другую сеть - Условия использования - К сожалению, текущая версия приложения не готова к работе с этой картой, проверьте наличие обновлений - Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком. - Мои токены - У вас нет добавленных токенов. Добавьте токены для обмена - Токены не найдены. Пожалуйста, попробуйте другой запрос - Недоступен для обмена с %s - Кроме того, в курс обмена включена комиссия сети за отправку обмененных средств на ваш адрес - Статус - Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами - Выберите провайдера - Курс обмена - Больше провайдеров на подходе.\nСледите за обновлениями! - Чтобы вернуть ваши деньги, посетите сайт провайдера - Операция не выполнена провайдером - Посетите сайт провайдера для проверки - Провайдер запрашивает прохождение верификации - Отменен - Подтверждено - Подтверждение - Подтверждение... - Обменяно - Обмен - Обмен... - Неудачно - Депозит получен - Ожидание депозита - Ожидаем пополнения... - Возвращено - Отправляем - Отправка средств... - Отправлено - Верифицировано - Требуется верификация - Список токенов в вашем кошельке - Получение наилучших курсов... - Плавающая ставка - К провайдеру - Пользуясь сервисом вы соглашаетесь с %s - Пользуясь сервисом вы соглашаетесь с %1$s и %2$s - Политикой конфиденциальности - Провайдер - Лучший курс - Доступно с %s - Доступно до %s - Недоступно для этой пары - Требуется разрешение - Условиями использования - Предоставлено - Информация ниже не является обязательной. Вы можете стереть её, если хотите. - Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. - Скажите, пожалуйста, какая у вас карта? - Привет, команда поддержки, - Пожалуйста, расскажите нам больше о вашей проблеме. Каждая маленькая деталь может помочь. - Мои предложения - Не могу отсканировать карту - Обращение в поддержку - Обращение в поддержку Tangem - Не могу отправить транзакцию - Купить - Сканировать - Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции - Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции - Чтобы создать кошелек, приложите карту как показано выше и не убирайте до окончания операции - Чтобы сбросить настройки до заводских, приложите карту как показано выше и не убирайте до окончания операции - Приложите, чтобы отсканировать - Чтобы подписать транзакцию, приложите карту как показано выше и не убирайте до окончания операции - Приложите, чтобы подписать - Приложите карту - Внутренняя ошибка: не удается найти менеджер кошельков - Вы обновили данные биометрии, отсканируйте свою карту для входа - Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены - Вы успешно прошли все уроки и теперь можете получить 1INCH токены - - Пройдите 3 урока и получите %d 1INCH токен на свой кошелек - Пройдите 3 урока и получите %d 1INCH токена на свой кошелек - Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек - Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек - - Управление токенами - Баланс - В сумме учтены не все монеты - 1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов - Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту - Отсканируйте карту - Обменивайте свои токены с %1$s комиссии провайдера через Changelly с %2$s по %3$s февраля. - Обмен с Changelly, %s комиссии - Токены - Добавить - Изменить - Основная сеть - Не основной или основной блокчейн, на котором размещен токен - Не основные сети - Сети - Выберите сети - Кошелек - Не удалось найти этот токен, вы можете добавить его вручную. - - %1$d из %2$d кошелька - %1$d из %2$d кошельков - %1$d из %2$d кошельков - %1$d из %2$d кошельков - - например Bitcoin - Рыночная капитализация - Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. - Голосовать - Выберите кошелек - Кошелёк не поддерживает более одной сети - Вам необходимо установить единый код доступа для защиты всех ваших карт - Защита - Позже вы сможете установить индивидуальный код доступа для каждой карты - Персонализация - Код доступа можно восстановить с помощью привязанной карты. Не храните все карты в одном месте - Восстановление - Выберите любое слово, фразу или число в качестве кода доступа - Создайте код доступа - Повторно введите код доступа - Код доступа должен состоять не менее чем из 4 символов. - Введенные коды доступа не совпадают - Необходимо повторить операцию, при этом карта будет сброшена к заводским настройкам - Ошибка активации - Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить? - Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас. - Добавить резервную карту - Сканировать карту #%d - Создать резервную копию - Сканировать основную карту - Запросить - Перейти к моему кошельку - Завершение бэкапа - Установить Код доступа - Получить криптовалюту - Сканировать основную карту - Пропустить - Как это работает? - Давайте сгенерируем все ключи на вашей карте и создадим безопасный кошелек - Создать кошелек - Создать кошелек - Другие опции - Ваши ключи будут надежно сгенерированы внутри карты. Никакой seed-фразы, а это значит, что никто не может экспортировать или украсть ее. - Cоздавайте ключи приватно - Ваша карта активирована и готова к использованию - Успешно! - В этом случае вам будет необходимо начать процесс заново. - Вы хотите выйти из процесса активации? - Подготовка - Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа? - Код доступа - Подключиться - Резервная копия - Прочитать о seed-фразе - Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте. - Ваша seed-фраза - Чтобы импортировать кошелек, введите seed-фразу в поле ниже - Создать seed-фразу - Импорт кошелька - Seed-фраза — это набор слов, который дает возможность восстановить кошелек. В отличие от ключей, сгенерированных картой, seed-фраза не защищена и может быть скопирована и украдена. Используйте этот вариант на свой страх и риск. - Использовать seed-фразу - Неверная seed-фраза. Пожалуйста, проверьте порядок слов. - Неверная seed-фраза. Пожалуйста, проверьте орфографию. - Устаревший - Чтобы проверить, правильно ли вы записали seed-фразу, введите 2-е, 7-е и 11-е слова - Итак, проверим - Для начала работы просто запросите начисление wxDAI на свой кошелек - Это займет несколько секунд - Чтобы начать процесс резервного копирования, добавьте одну или две резервные карты. - Вы можете добавить еще одну карту или завершить процесс резервного копирования - Подготовьте резервную карту с номером %s - Отсканируйте основную карту, чтобы начать процесс резервного копирования. - Подготовьте основную карту с номером %s - Поздравляем! Ваша платежная крипто карта теперь активирована! - Ваша карта настроена и готова к использованию. - Добавлено максимальное количество карт. Завершите процесс резервного копирования. - Активация карты - Резервная карта #%d - Запросить %s - Запрашивается - Нет резервных карт - Добавлена ​​одна резервная карта - Код доступа - Подготовьте свою карту - Добавлены две резервные карты - Пополните кошелек на любую сумму, чтобы начать пользоваться картой - Пополните кошелек более чем на %1$s %2$s, чтобы начать пользоваться картой - Купить криптовалюту - Показать адрес кошелька - Активация кошелька - Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас. - Если процесc создания кошелька каким-либо образом прервется, вам придется начинать сначала - Вы можете сделать резервную копию своих ключей на одной или двух других пустых картах Wallet. - Код доступа можно восстановить с помощью одной из резервных карт. - Все резервные карты являются полнофункциональными и содержат одинаковые ключи. - Вы сможете установить код доступа для защиты своих кошельков. - Резервная копия карты - Восстановление кода доступа - Идентичные карты - Код доступа - Группы - По балансу - Сортировка токенов - Список - %1$s %2$s адрес в сети %3$s - %1$s (%2$s) в сети %3$s - Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств. - Участвовать - Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже. - Не удалось загрузить информацию по реферальной программе. Код ошибки: %s. Пожалуйста, попробуйте позже. - Грядущие выплаты - Ваши друзья купили - Меньше - Больше - Нет грядущих выплат - - за %d кошелек - за %d кошелька - за %d кошельков - за %d кошельков - - Получите ^^%1$s^^ на ваш адрес в сети %2$s %3$s ^^спустя 30 дней^^ за каждый кошелек, который купит ваш друг - Получите - на ваш адрес в сети %1$s%2$s за каждый кошелек, который купит ваш друг - Вы - Получит - при покупке кошелька на сайте tangem.com - %s скидку - Ваш друг - Персональный код скопирован! - Ваш персональный код - Купи Tangem Wallet со скидкой!\n%s - Приведи друга в Tangem - Вы приняли - Нажимая на эту кнопку, вы принимаете - в реферальной программе - - %d кошелек - %d кошелька - %d кошельков - %d кошельков - - Пожалуйста, удерживайте карту до завершения операции - Сбросить карту - Я понимаю, что после выполнения этого действия у меня больше не будет доступа к текущему кошельку - Я понимаю, что не смогу этой картой восстановить пароль на остальных картах этого кошелька, если я его забуду - Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек или использовать данную карту для восстановления кода доступа. - Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек. - У вас есть карта банка другой страны, а также вид на жительство или регистрация вне РФ? - Карты банков РФ в данный момент не принимаются - Войдите в приложение и следите за своим балансом без сканирования карты - Доступ в приложение - Использовать биометрию - Для операций с вашим кошельком будет запрашиваться биометрия вместо кода доступа карты - Код доступа - Похоже, что у вас отключена биометрическая аутентификация, она необходима для сохранения кошельков - Включите биометрическую аутентификацию - Вы хотите использовать биометрию? - Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта - Ошибка сканирования карты. Попробуй снова. - Сканировать - Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку. - Приготовьте свою карту - Уже содержится в введенном адресе - Сумма - Транзакция не выполнена - Причина: %1$s\nКод: %2$s - Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму? - Вычесть - %1$s в %2$s - Адрес - Код назначения - Введите адрес - Адрес совпадает с адресом кошелька - Недопустимый Tag. Он не будет добавлен в транзакцию. - Недопустимый Memo. Он не будет добавлен в транзакцию. - Tag - Memo - Включая комиссию - Низкая - Нормальная - Приоритетная - Лимит газа - Цена газа - Цена газа влияет на скорость транзакции. При сильно низкой, транзакция может быть не обработана. - Всё - Максимальная сумма - Комиссия не превысит - Максимальная cумма комиссии - Сетевая комиссия - Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s. - Покрытие сетевой комиссии - Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса - Недостаточно средств - Увеличение комиссии - Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. - Установлена высокая комиссия - Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. - Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению - Недопустимая сумма - Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s. - Адрес получателя не активирован. Пожалуйста, измените сумму отправки, чтобы продолжить. - Сумма отправки не может быть менее %1$s - Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции - Возможны задержки по транзакции - Лимит транзакции - Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. - Уменьшить до %s - Уменьшить на %s - Необязательное - Последние - Получатель - Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов - Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. - Мои кошельки - Отправка %s - Всего - %1$s и %2$s будет отправлено - ≈ %1$s (вкл. комиссию: %2$s) - %s будет отправлено - Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время - Неверный адрес - Купить сейчас - У меня есть промо-код… - Tangem Wallet - Другие способы оплаты - Сделать предзаказ - Итого - Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. - Революционный аппаратный кошелек - До трех карт с одним кошельком - До - трех карт - с одним кошельком - Все ключи в безопасности - Аппаратный кошелек для ваших биткоинов, эфира и многих других валют одновременно — все в одной карте - Тысячи криптовалют - Используйте его на ходу, в любом месте, в любое время. Без проводов и батареек. Как только понадобится крипта, просто приложите карту к телефону. - Кошелек для каждого - Пройдите 3 урока, получите скидку на покупку Tangem Wallet и 1INCH токены - Пройти обучение - Встречайте Tangem - Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах - Поддержка Web 3.0 - Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. - Подтвердить - Ошибка: %s - Вы отправляете - Дать разрешение - Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. - Недостаточно средств - Комиссии - В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя - В сумму включена комиссия провайдера сервиса. - Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии - Недостаточно средств для оплаты комиссии на вашем %1$s кошельке для создания транзакции. Сначала пополните свой %2$s кошелек. - Транзакция в процессе… - Подождите - Подтвердить - Текущая транзакция - Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. - Дать разрешение - Укажите лимит доступа к выбранному токену - Количество %s - Отправитель - Ваш кошелек - Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s - Безлимитно - Открыть в обозревателе - В процессе - Обменять - Обмен %s на - Котировки включают дополнительную комиссию Tangem в размере %s. Это помогает нам предоставлять первоклассный продукт. - Вы получите - Другие токены - Выберите токен - Ваши токены - не доступен - Балансы скрыты - Балансы показаны - Отменить - - %d токен - %d токена - %d токенов - %d токенов - - Скрыть - Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. - Скрыть %s - Скрыть токен - %1$s токен в сети %%image%% %2$s - Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. - Невозможно скрыть %s - Нет цены - Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля. - Обмен с Changelly, %s комиссии - Обменять - контракт: %s - У вас еще нет транзакций - Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию. - Несколько адресов - История транзакций в настоящее время не поддерживается для этого блокчейна. Но не волнуйтесь, мы работаем над этим! А пока вы можете проверить ее в обозревателе. - Операция - от: %s - на: %s - В процессе… - Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d - Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую - Это карта, которую вы держите в руках. У парной карты номер %s.\n\nОбе карты можно использовать для вывода средств из этого кошелька. - Один кошелек. Две карты. - Сканировать карту #%s - Создание кошелька - Отсканируйте twin-карту #%s - Подготовка карты - Tangem Twin - Это действие необратимо. У вас не будет доступа к старому кошельку. - Приложите twin-карту с номером %s и не убирайте до окончания операции - Используйте %s или отсканируйте карту, чтобы получить доступ к своему кошельку - Добавить новый кошелек - Вы уверены, что хотите удалить этот кошелек? - %d выбрано - Произошла ошибка, пожалуйста, отсканируйте свою карту для входа - Этот кошелек уже был сохранен, вы можете добавить другой - Мультивалютные - Имя кошелька - Переименование кошелька - Одновалютные - Мои кошельки - Разблокировать все - Разблокировать все с %s - История транзакций - Блокчейн недоступен. Попробуйте позже. - Баланс загружается… - Отсканируйте карту - Транзакция подтверждается… - Подтвержденный баланс - Действия - Что отправить? - Вы хотите купить или продать криптовалюту? - Запрос на подпись сообщения.\n\n%s - Dapp %1$s, запрос на\nподпись транзакции с BNB.\n\n%2$s - Торговый ордер на %1$s\nЦена: %2$s\nСумма к получению: %3$s\nСумма к оплате: %4$s - Детали транзакции:\nОт: %1$s\nК: %2$s\nСумма: %3$s - Транзакция с BNB успешно подписана и отправлена ​​в Dapp. - Буфер обмена содержит код WalletConnect. Использовать скопированное значение или отсканировать QR-код - Запрос на создание транзакции для %1$s\n%2$s\n\nСумма: %3$s\nКомиссия: %4$s\nВсего: %5$s\nБаланс: %6$s - Невозможно отправить транзакцию. Недостаточно средств. - Не удалось установить сессию WalletConnect. Пожалуйста, повторите попытку позже. - Не все токены добавлены в ваш список. Пожалуйста, добавьте их в начале, а потом попробуйте снова. Недостающие токены: \n - Не удалось подписать сообщение.\nПожалуйста, попробуйте еще раз - Не удалось установить сессию WalletConnect за отведённое время. Пожалуйста, повторите попытку позже. - Запрос на подключение через WalletConnect содержит неподдерживаемые блокчеины. Неподдерживаемые блокчеины:\n - Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации. - Произошла непредвиденная ошибка. Сообщение ошибки: %s Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. - Неверная карта выбрана в приложении Tangem - Не удалось создать транзакцию из данных Dapp. Код: %s - Произошла непредвиденная ошибка. Код ошибки: %d Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. - Сообщение было успешно подписано и отправлено в Dapp - Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново. - Нет открытых сессий WalletConnect - Упс. Нет сессий. - Не удалось создать пару WalletConnect: %1$s - Вставить из буфера обмена - Сообщение для %1$s:\n%2$s - Запрос на открытие сессии для\n%1$s\n\nСЕТЬ: %2$s\n\nURL: %3$s - Операция не может быть завершена.\n\nВы уже установили сеанс WalletConnect с этими параметрами. - Сканировать новый код - Эту карту нельзя использовать с WalletConnect. - Сеть не поддерживается. Пожалуйста, выберите другую сеть. - Выберите сеть - Dapp не предоставил необходимые данные для открытия сессии WalletConnect - Не удалось найти сессию для обработки запроса - Сессии WalletConnect - Подключение к dApps - WalletConnect - Транзакция успешно подписана и отправлена ​​в Dapp - Транзакция успешно подписана и отправлена ​​в блокчейн - Не удалось найти хэш транзакции - Сеть %s - Аккаунт не создан - Эта карта не поддерживается - Ваша карта Tangem предназначена для работы с другим приложением. Пожалуйста, ознакомьтесь с названием и инструкциями на вашей карте и установите правильное приложение. - Рыночная цена %s - за 24 часа - Сеть %s - Адрес скопирован в буфер обмена - Нет соединения с интернетом - Получение - от %s - Отправка - к %s - Tangem - Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку - Можно лучше - Узнать больше - Нравится - Понятно! - Очень круто! - Обновить - Вы находитесь в режиме демо - Демо режим включен - Отсканированная вами карта является картой разработчика. Не используйте ее для создания своего кошелька. - Не для пользователя! - Cеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета будет ниже %2$s, то он будет деактивирован, а средства на счете уничтожены. - Для работы с сетью необходим депозит - Обмен будет доступен после завершения %s транзакции - У вас есть активная транзакция - У вас в списке нет монет доступных для обмена с %s - Нет доступных токенов для обмена - Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s - Невозможно покрыть комиссию %s - Cервис временно недоступен - Пожалуйста, измените сумму для обмена - Сумма для обмена должна быть не менее %s - Сумма для обмена должна быть не более %s - Возможно, данная карта - образец или подделка - Ошибка проверки подлинности - На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. - Малое количество подписей - Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети. - - Используйте вашу карту, чтобы получить адрес для %d сети - Используйте вашу карту, чтобы получить адреса для %d сетей - Используйте вашу карту, чтобы получить адреса для %d сетей - Используйте вашу карту, чтобы получить адреса для %d сетей - - Некоторые адреса отсутствуют - В данный момент сеть недоступна. Пожалуйста, попробуйте позже. - Сеть недоступна - Пополните ваш кошелек - Ваш кошелек не имеет резервной копии. Проведите эту процедуру сейчас, чтобы защитить ваши активы. - Резервная копия отсутствует - Эта карта ранее использовалась для подписи транзакций. Если она получена от ненадежного источника, рассмотрите возможность вывода своих средств. Если это ваша карта, дополнительных действий не требуется. - Карта уже подписывала транзакции - Устройство несовместимо - Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше - Нравится Tangem? - Необходима плата за аренду сети - %1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети. - Недостаточно %1$s для оплаты комиссии сети - Отправка средств станет доступной после завершения транзакции %s - Транзакция в обработке - Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. - Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже. - Некоторые сети недоступны - Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки. - Только для целей тестирования - Отказаться - Вы не закончили резервное копирование. Хотите продолжить? - Да, возобновить - Отказаться - Если сейчас отказаться, то придётся сбрасывать карты до заводских настроек, чтобы начать заново - Возобновить резервное копирование - Это необратимое действие - Войти с %s - Сканировать карту - Используйте %s или отсканируйте карту для входа в приложение - C возвращением! - Обмен через %s - Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий. - Статус обмена + Добавить токен + Валюты + Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. + Спасибо за ваш отзыв + Отправлено успешно + Обратиться в поддержку + Отправить отзыв + Эта функция недоступна в демонстрационном режиме + Не удалось отправить письмо + Причина: %s + Не могу отправить транзакцию + Выбранный кошелёк не поддерживает сеть %1$s + Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен. + Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки. + У вас возникли трудности со сканированием карты? + Эта карта не предназначена для работы с этим приложением + Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem + Включите биометрическую аутентификацию + Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком. + При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены из приложения. + Сохранение кода доступа + Подключите функцию хранения кодов доступа от карт на телефоне в зашифрованном виде, и при работе с картой вместо кода доступа будет запрашиваться биометрическая аутентификация. + Cохранение кошелька + Подключите функцию привязки карты в приложении, а также возможность биометрической аутентификации. Подпись транзакции все так же потребует карту. + Тёмная + Светлая + Как в системе + Тема + Настройки приложения + Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\" + Больше не показывать + Понятно + Балансы скрыты + Пожалуйста, отсканируйте карту + Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту + Слишком много попыток + Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона. + Начать резервное копирование + + %d карта + %d карты + %d карт + %d карт + + Отключите эту опцию, если не хотите, чтобы эта карта использовалась для сброса кодов доступа на другие карты этого кошелька. Обратите внимание, сброс кода также не будет доступен на этой карте. + Использовать эту карту для сброса кода доступа на других картах в этом кошельке + Отключить возможность сброса кода доступа на этой карте или других картах этого кошелька + Восстановление кода доступа + Сбросить + Вы уверены, что хотите это сделать? + Смена кода доступа + Код доступа будет изменен только на данной карте + Заводские настройки + Тип безопасности + Выбранный способ защиты приложения + Настройки карты + Tangem Bot + Чат + Отправить логи + Принять + Доступ запрещен + Добавить + Применить + Одобрение + Внимание + Баланс: %s + Баланс + биометрическую аутентификацию + биометрией + Купить + Перейти на %1$s + Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности. + Отмена + Закрыть + Продолжить + Копировать + Скопировать адрес + Создать + Удалить + Отключено + Не нравится + Готово + Включить + Включено + Ошибка + Обменять + Обозреватель + Посмотреть историю + Посмотреть историю транзакций + Обозреватель + Комиссия + Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s + Свое + Быстро + По рынку + Медленно + Скорость и комиссия + Получить адреса + К провайдеру + Импортировать + Нравится + Заблокирован + Основная сеть + Сетевая комиссия + Далее + Нет + Нет адреса + Нет данных + OK + Основная карта + Вставить + Подробнее + Получить + Отклонить + Перезагрузить + Переименовать + Сохранить изменения + Искать + Поиск токенов + Seed-фраза + Выберите действие + Продать + Отправить + Сервер недоступен, повторите попытку позднее + Поделиться + Подписать + Подписать и отправить + Стейкинг + Начать + Отправить + Успешно + Поддержка + Обмен + условия участия + Ошибка транзакции + Транзакции + Перевод + Я понял + Произошла ошибка. Пожалуйста, попробуйте снова. + Недоступно + Да + Адрес контракта скопирован! + Доступные сети + Добавить токен + Адрес контракта + Адрес контракта некорректен + Путь деривации некорректен + Пожалуйста, выберите сеть + Обязательное поле + Своя деривация + Например m/00\'/0000\'/0\'/0/0 + Введите свою деривацию + Знаков после запятой + Путь деривации + По умолчанию + Деривация по BIP44 + Введенный путь деривации некорректен + Например, USD Coin + Название токена + Не выбрано + Сеть + Сеть + Вы можете добавить токен в ручную, если он не поддерживается Tangem + Например, USDC + Символ + Символ токена + Этот токен/сеть уже находится в вашем списке + Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить + Остерегайтесь мошеннических токенов, они могут ничего не стоить + Токены могут быть созданы кем угодно + Чат + Код доступа + Перед сканированием карты вам нужно будет ввести правильный код доступа. + Задержка сканирования + Этот механизм защищает карту от бесконтактных атак. Между сканированием карты и выполнением команды будет добавлена задержка. + Пароль + Перед выполнением любой команды, влекущей за собой изменение состояния карты, вам необходимо будет ввести пароль. + Реферальная программа + Переверните экран вашего устройства вниз, чтобы быстро скрыть и отобразить балансы + Privacy policy + %s хэшей + Номер карты + Обратиться в поддержку + Добавить еще карты + Валюта приложения + Скрывать балансы жестом переворота + Эмитент + Подписано + Подробности + Проверьте подключение с интернетом или переключитесь на другую сеть + Условия использования + К сожалению, текущая версия приложения не готова к работе с этой картой, проверьте наличие обновлений + Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком. + Мои токены + У вас нет добавленных токенов. Добавьте токены для обмена + Недоступен для обмена с %s + Предоставлено + Предоставлено %s + Кроме того, в курс обмена включена комиссия сети за отправку обмененных средств на ваш адрес + Статус + Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами + Выберите провайдера + Произошла ошибка. Код: %s + Курс обмена + Обмен через %s + Чтобы вернуть ваши деньги, посетите сайт провайдера + Операция не выполнена провайдером + Посетите сайт провайдера для проверки + Провайдер запрашивает прохождение верификации + Отменен + Подтверждено + Подтверждение + Подтверждение... + Обменяно + Обмен + Обмен... + Неудачно + Депозит получен + Ожидание депозита + Ожидаем пополнения... + Возвращено + Возврат... + Отправляем + Отправка средств... + Отправлено + Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий. + Статус обмена + Верифицировано + Требуется верификация + Список токенов в вашем кошельке + Получение наилучших курсов... + Плавающая ставка + Пользуясь сервисом, вы соглашаетесь с %s + Пользуясь сервисом, вы соглашаетесь с %1$s и %2$s + Больше провайдеров на подходе.\nСледите за обновлениями! + Политикой конфиденциальности + Провайдер + Лучший курс + Доступно до %s + Доступно с %s + Недоступно для этой пары + Требуется разрешение + Условиями использования + Токены не найдены. Пожалуйста, попробуйте другой запрос + ID транзакции скопирован + Информация ниже не является обязательной. Вы можете стереть её, если хотите. + Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. + Скажите, пожалуйста, какая у вас карта? + Привет, команда поддержки, + Пожалуйста, расскажите нам больше о вашей проблеме. Каждая маленькая деталь может помочь. + Мои предложения + Не могу отсканировать карту + Обращение в поддержку + Обращение в поддержку Tangem + Не могу отправить транзакцию + Купить + Сканировать + Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции + Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции + Чтобы создать кошелек, приложите карту как показано выше и не убирайте до окончания операции + Чтобы сбросить настройки до заводских, приложите карту как показано выше и не убирайте до окончания операции + Приложите, чтобы отсканировать + Чтобы подписать транзакцию, приложите карту как показано выше и не убирайте до окончания операции + Приложите, чтобы подписать + Приложите карту + Внутренняя ошибка: не удается найти менеджер кошельков + Вы обновили данные биометрии, отсканируйте свою карту для входа + Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены + Вы успешно прошли все уроки и теперь можете получить 1INCH токены + + Пройдите 3 урока и получите %d 1INCH токен на свой кошелек + Пройдите 3 урока и получите %d 1INCH токена на свой кошелек + Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек + Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек + + Управление токенами + Баланс + В сумме учтены не все монеты + 1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов + Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту + Отсканируйте карту + Обменивайте свои токены с %1$s комиссии провайдера через Changelly с %2$s по %3$s февраля. + Обмен с Changelly, %s комиссии + Токены + Добавить + Изменить + Основная сеть + Не основной или основной блокчейн, на котором размещен токен + Не основные сети + Сети + Выберите сети + Кошелек + Не удалось найти этот токен, вы можете добавить его вручную. + + %1$d из %2$d кошелька + %1$d из %2$d кошельков + %1$d из %2$d кошельков + %1$d из %2$d кошельков + + например Bitcoin + Рыночная капитализация + Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. + Голосовать + Выберите кошелек + Кошелёк не поддерживает более одной сети + Вам необходимо установить единый код доступа для защиты всех ваших карт + Защита + Позже вы сможете установить индивидуальный код доступа для каждой карты + Персонализация + Код доступа можно восстановить с помощью привязанной карты. Не храните все карты в одном месте + Восстановление + Выберите любое слово, фразу или число в качестве кода доступа + Создайте код доступа + Повторно введите код доступа + Код доступа должен состоять не менее чем из 4 символов. + Введенные коды доступа не совпадают + Необходимо повторить операцию, при этом карта будет сброшена к заводским настройкам + Ошибка активации + Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить? + Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас. + Добавить резервную карту + Сканировать карту #%d + Создать резервную копию + Сканировать основную карту + Запросить + Перейти к моему кошельку + Завершение бэкапа + Установить Код доступа + Получить криптовалюту + Сканировать основную карту + Пропустить + Как это работает? + Давайте сгенерируем все ключи на вашей карте и создадим безопасный кошелек + Создать кошелек + Создать кошелек + Другие опции + Ваши ключи будут надежно сгенерированы внутри карты. Никакой seed-фразы, а это значит, что никто не может экспортировать или украсть ее. + Cоздавайте ключи приватно + Ваша карта активирована и готова к использованию + Успешно! + В этом случае вам будет необходимо начать процесс заново. + Вы хотите выйти из процесса активации? + Подготовка + Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа? + Код доступа + Подключиться + Резервная копия + Прочитать о seed-фразе + Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте. + Ваша seed-фраза + Чтобы импортировать кошелек, введите seed-фразу в поле ниже + Создать seed-фразу + Импорт кошелька + Seed-фраза — это набор слов, который дает возможность восстановить кошелек. В отличие от ключей, сгенерированных картой, seed-фраза не защищена и может быть скопирована и украдена. Используйте этот вариант на свой страх и риск. + Использовать seed-фразу + Неверная seed-фраза. Пожалуйста, проверьте порядок слов. + Неверная seed-фраза. Пожалуйста, проверьте орфографию. + Устаревший + Чтобы проверить, правильно ли вы записали seed-фразу, введите 2-е, 7-е и 11-е слова + Итак, проверим + Для начала работы просто запросите начисление wxDAI на свой кошелек + Это займет несколько секунд + Чтобы начать процесс резервного копирования, добавьте одну или две резервные карты. + Вы можете добавить еще одну карту или завершить процесс резервного копирования + Подготовьте резервную карту с номером %s + Отсканируйте основную карту, чтобы начать процесс резервного копирования. + Подготовьте основную карту с номером %s + Поздравляем! Ваша платежная крипто карта теперь активирована! + Ваша карта настроена и готова к использованию. + Добавлено максимальное количество карт. Завершите процесс резервного копирования. + Активация карты + Резервная карта #%d + Запросить %s + Запрашивается + Нет резервных карт + Добавлена ​​одна резервная карта + Код доступа + Подготовьте свою карту + Добавлены две резервные карты + Пополните кошелек на любую сумму, чтобы начать пользоваться картой + Пополните кошелек более чем на %1$s %2$s, чтобы начать пользоваться картой + Купить криптовалюту + Показать адрес кошелька + Активация кошелька + Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас. + Если процесc создания кошелька каким-либо образом прервется, вам придется начинать сначала + Вы можете сделать резервную копию своих ключей на одной или двух других пустых картах Wallet. + Код доступа можно восстановить с помощью одной из резервных карт. + Все резервные карты являются полнофункциональными и содержат одинаковые ключи. + Вы сможете установить код доступа для защиты своих кошельков. + Резервная копия карты + Восстановление кода доступа + Идентичные карты + Код доступа + Группы + По балансу + Сортировка токенов + Список + Отправить отзыв + Оцените приложение 🙌 + Поделитесь опытом использования приложения - нам важна любая деталь. + Нравится наше приложение? + %1$s %2$s адрес в сети %3$s + %1$s (%2$s) в сети %3$s + Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств. + Участвовать + Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже. + Не удалось загрузить информацию по реферальной программе. Код ошибки: %s. Пожалуйста, попробуйте позже. + Грядущие выплаты + Ваши друзья купили + Меньше + Больше + Нет грядущих выплат + + за %d кошелек + за %d кошелька + за %d кошельков + за %d кошельков + + Получите ^^%1$s^^ на ваш адрес в сети %2$s %3$s ^^спустя 30 дней^^ за каждый кошелек, который купит ваш друг + Получите + на ваш адрес в сети %1$s%2$s за каждый кошелек, который купит ваш друг + Вы + Получит + при покупке кошелька на сайте tangem.com + %s скидку + Ваш друг + Персональный код скопирован! + Ваш персональный код + Купи Tangem Wallet со скидкой!\n%s + Приведи друга в Tangem + Вы приняли + Нажимая на эту кнопку, вы принимаете + в реферальной программе + + %d кошелек + %d кошелька + %d кошельков + %d кошельков + + Пожалуйста, удерживайте карту до завершения операции + Сбросить карту + Я понимаю, что после выполнения этого действия у меня больше не будет доступа к текущему кошельку + Я понимаю, что не смогу этой картой восстановить пароль на остальных картах этого кошелька, если я его забуду + Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек или использовать данную карту для восстановления кода доступа. + Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек. + У вас есть карта банка другой страны, а также вид на жительство или регистрация вне РФ? + Карты банков РФ в данный момент не принимаются + Войдите в приложение и следите за своим балансом без сканирования карты + Доступ в приложение + Использовать биометрию + Для операций с вашим кошельком будет запрашиваться биометрия вместо кода доступа карты + Код доступа + Похоже, что у вас отключена биометрическая аутентификация, она необходима для сохранения кошельков + Включите биометрическую аутентификацию + Вы хотите использовать биометрию? + Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта + Ошибка сканирования карты. Попробуй снова. + Сканировать + Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку. + Приготовьте свою карту + Уже содержится в введенном адресе + Вычесть + Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму? + Комиссия увеличилась + Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? + Причина: %1$s\nКод: %2$s + Транзакция не выполнена + Сумма + Подтвердить + %1$s в %2$s + Адрес + Код назначения + Введите адрес + Адрес совпадает с адресом кошелька + Недопустимый Tag. Он не будет добавлен в транзакцию. + Недопустимый Memo. Он не будет добавлен в транзакцию. + Tag + Memo + Включая комиссию + Низкая + Нормальная + Приоритетная + Лимит газа + Цена газа + Цена газа влияет на скорость транзакции. При сильно низкой, транзакция может быть не обработана. + Всё + Максимальная сумма + Комиссия не превысит + Максимальная cумма комиссии + Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s. + Покрытие сетевой комиссии + Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса + Недостаточно средств + Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети. + Комиссия превышает баланс + Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. + Установлена высокая комиссия + Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s. + Увеличение комиссии + Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению + Недопустимая сумма + Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s. + Адрес получателя не активирован. \nПожалуйста, измените сумму отправки, чтобы продолжить. + Сумма отправки не может быть менее %s + Уменьшить на %s + Уменьшить до %s + Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции + Возможны задержки по транзакции + Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. + Лимит транзакции + Необязательное + Последние + Получатель + Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов + Отправить + Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. + Мои кошельки + Отправка %s + Всего + %1$s и %2$s будет отправлено + ≈ %1$s (вкл. комиссию: %2$s) + %s будет отправлено + Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время + Неверный адрес + Купить сейчас + У меня есть промо-код… + Tangem Wallet + Другие способы оплаты + Сделать предзаказ + Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. + Революционный аппаратный кошелек + До трех карт с одним кошельком + До + трех карт + с одним кошельком + Все ключи в безопасности + Аппаратный кошелек для ваших биткоинов, эфира и многих других валют одновременно — все в одной карте + Тысячи криптовалют + Используйте его на ходу, в любом месте, в любое время. Без проводов и батареек. Как только понадобится крипта, просто приложите карту к телефону. + Кошелек для каждого + Пройдите 3 урока, получите скидку на покупку Tangem Wallet и 1INCH токены + Пройти обучение + Встречайте Tangem + Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах + Поддержка Web 3.0 + В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя + В сумму включена комиссия провайдера сервиса. + Комиссии + Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. + Подтвердить + Ошибка: %s + Вы отправляете + Дать разрешение + Сильные колебания цены! + Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. + Недостаточно средств + Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии + Недостаточно средств для оплаты комиссии на вашем %1$s кошельке для создания транзакции. Сначала пополните свой %2$s кошелек. + Транзакция в процессе… + Подождите + Подтвердить + Текущая транзакция + Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. + Дать разрешение + Укажите лимит доступа к выбранному токену + Количество %s + Отправитель + Ваш кошелек + Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s + Безлимитно + Открыть в обозревателе + В процессе + Обменять + Обмен %s на + Котировки включают дополнительную комиссию Tangem в размере %s. Это помогает нам предоставлять первоклассный продукт. + Вы получите + Другие токены + Выберите токен + Ваши токены + не доступен + Балансы скрыты + Балансы показаны + Отменить + + %d токен + %d токена + %d токенов + %d токенов + + Скрыть + Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. + Скрыть %s + Скрыть токен + %1$s токен в сети %%image%% %2$s + Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. + Невозможно скрыть %s + Нет цены + Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля. + Обмен с Changelly, %s комиссии + Обменять + контракт: %s + У вас еще нет транзакций + Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию. + Несколько адресов + История транзакций в настоящее время не поддерживается для этого блокчейна. Но не волнуйтесь, мы работаем над этим! А пока вы можете проверить ее в обозревателе. + Операция + от: %s + на: %s + В процессе… + Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d + Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую + Это карта, которую вы держите в руках. У парной карты номер %s.\n\nОбе карты можно использовать для вывода средств из этого кошелька. + Один кошелек. Две карты. + Сканировать карту #%s + Создание кошелька + Отсканируйте twin-карту #%s + Подготовка карты + Tangem Twin + Это действие необратимо. У вас не будет доступа к старому кошельку. + Приложите twin-карту с номером %s и не убирайте до окончания операции + Используйте %s или отсканируйте карту, чтобы получить доступ к своему кошельку + Добавить новый кошелек + Вы уверены, что хотите удалить этот кошелек? + %d выбрано + Произошла ошибка, пожалуйста, отсканируйте свою карту для входа + Этот кошелек уже был сохранен, вы можете добавить другой + Мультивалютные + Имя кошелька + Переименование кошелька + Одновалютные + Мои кошельки + Разблокировать все + Разблокировать все с %s + История транзакций + Блокчейн недоступен. Попробуйте позже. + Баланс загружается… + Отсканируйте карту + Транзакция подтверждается… + Подтвержденный баланс + Действия + Что отправить? + Вы хотите купить или продать криптовалюту? + Запрос на подпись сообщения.\n\n%s + Dapp %1$s, запрос на\nподпись транзакции с BNB.\n\n%2$s + Торговый ордер на %1$s\nЦена: %2$s\nСумма к получению: %3$s\nСумма к оплате: %4$s + Детали транзакции:\nОт: %1$s\nК: %2$s\nСумма: %3$s + Транзакция с BNB успешно подписана и отправлена ​​в Dapp. + Буфер обмена содержит код WalletConnect. Использовать скопированное значение или отсканировать QR-код + Запрос на создание транзакции для %1$s\n%2$s\n\nСумма: %3$s\nКомиссия: %4$s\nВсего: %5$s\nБаланс: %6$s + Невозможно отправить транзакцию. Недостаточно средств. + Не удалось установить сессию WalletConnect. Пожалуйста, повторите попытку позже. + Не все токены добавлены в ваш список. Пожалуйста, добавьте их в начале, а потом попробуйте снова. Недостающие токены: \n + Не удалось подписать сообщение.\nПожалуйста, попробуйте еще раз + Не удалось установить сессию WalletConnect за отведённое время. Пожалуйста, повторите попытку позже. + Запрос на подключение через WalletConnect содержит неподдерживаемые блокчеины. Неподдерживаемые блокчеины:\n + Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации. + Произошла непредвиденная ошибка. Сообщение ошибки: %s Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. + Неверная карта выбрана в приложении Tangem + Не удалось создать транзакцию из данных Dapp. Код: %s + Произошла непредвиденная ошибка. Код ошибки: %d Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. + Сообщение было успешно подписано и отправлено в Dapp + Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново. + Нет открытых сессий WalletConnect + Упс. Нет сессий. + Не удалось создать пару WalletConnect: %1$s + Вставить из буфера обмена + Сообщение для %1$s:\n%2$s + Запрос на открытие сессии для\n%1$s\n\nСЕТЬ: %2$s\n\nURL: %3$s + Операция не может быть завершена.\n\nВы уже установили сеанс WalletConnect с этими параметрами. + Сканировать новый код + Эту карту нельзя использовать с WalletConnect. + Сеть не поддерживается. Пожалуйста, выберите другую сеть. + Выберите сеть + Dapp не предоставил необходимые данные для открытия сессии WalletConnect + Не удалось найти сессию для обработки запроса + Сессии WalletConnect + Подключение к dApps + WalletConnect + Транзакция успешно подписана и отправлена ​​в Dapp + Транзакция успешно подписана и отправлена ​​в блокчейн + Не удалось найти хэш транзакции + Сеть %s + Аккаунт не создан + Эта карта не поддерживается + Ваша карта Tangem предназначена для работы с другим приложением. Пожалуйста, ознакомьтесь с названием и инструкциями на вашей карте и установите правильное приложение. + Рыночная цена %s + за 24 часа + Сеть %s + Адрес скопирован в буфер обмена + Нет соединения с интернетом + Получение + от %s + Отправка + к %s + Tangem + Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку + Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян. + Ошибка активации + Можно лучше + Узнать больше + Нравится + Понятно! + Очень круто! + Обновить + Вы находитесь в режиме демо + Демо режим включен + Отсканированная вами карта является картой разработчика. Не используйте ее для создания своего кошелька. + Не для пользователя! + Cеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета будет ниже %2$s, то он будет деактивирован, а средства на счете уничтожены. + Для работы с сетью необходим депозит + Обмен будет доступен после завершения %s транзакции + У вас есть активная транзакция + Разрешение обмена в процессе и будет скоро завершено + Разрешение в процессе + Минимальная сумма обмена - %1$s. Пожалуйста, убедитесь, что остаток после обмена также не будет меньше %2$s. + У вас в списке нет монет доступных для обмена с %s + Нет доступных для обмена токенов + Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s + Невозможно покрыть комиссию %s + Cервис временно недоступен + Сумма для обмена должна быть не более %s + Сумма для обмена должна быть не менее %s + Пожалуйста, измените сумму для обмена + Возможно, данная карта - образец или подделка + Ошибка проверки подлинности + На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. + Малое количество подписей + Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети. + + Используйте вашу карту, чтобы получить адрес для %d сети + Используйте вашу карту, чтобы получить адреса для %d сетей + Используйте вашу карту, чтобы получить адреса для %d сетей + Используйте вашу карту, чтобы получить адреса для %d сетей + + Некоторые адреса отсутствуют + В данный момент сеть недоступна. Пожалуйста, попробуйте позже. + Сеть недоступна + Пополните ваш кошелек + Ваш кошелек не имеет резервной копии. Проведите эту процедуру сейчас, чтобы защитить ваши активы. + Резервная копия отсутствует + Эта карта ранее использовалась для подписи транзакций. Если она получена от ненадежного источника, рассмотрите возможность вывода своих средств. Если это ваша карта, дополнительных действий не требуется. + Карта уже подписывала транзакции + Устройство несовместимо + Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше + Нравится Tangem? + Необходима плата за аренду сети + %1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети. + Недостаточно %1$s для оплаты комиссии сети + Отправка средств станет доступной после завершения транзакции(-ий) в сети %s + Отправка средств станет доступной после завершения транзакции %s + Транзакция в обработке + Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. + Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже. + Некоторые сети недоступны + Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки. + Только для целей тестирования + Отказаться + Вы не закончили резервное копирование. Хотите продолжить? + Да, возобновить + Отказаться + Если сейчас отказаться, то придётся сбрасывать карты до заводских настроек, чтобы начать заново + Возобновить резервное копирование + Это необратимое действие + Войти с %s + Сканировать карту + Используйте %s или отсканируйте карту для входа в приложение + C возвращением! diff --git a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml index eb43a8cf6f..7545da58e5 100644 --- a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml +++ b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml @@ -3,9 +3,12 @@ 默認 遺留資產 獲取費用失敗 - 由於 Kaspa 的限制,只有%1$d UTXO 可以放入單次交易中。這意味著您只能發送%2$s或更少數量。您需要減少數量。 - 要創建帳戶,請將資金發送到此地址 + 由於 %1$s 的限制,只有%2$d UTXO 可以放入單次交易中。這意味著您只能發送%3$s或更少數量。您需要減少數量。 + 沒有足夠的資金進行交易。請先入金 + + 目標帳戶未激活。發送 %s 或更多以激活帳戶 + 要創建帳戶,請將資金發送到此地址 最小數量是 %s 更動太小 無效費用 diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index bef56ab91b..b3e418f344 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -1,463 +1,459 @@ - 添加自定義代幣 - 管理代幣 - 僅將 %1$s (%2$s) 從 %3$s 網絡發送到此地址。使用其他代幣和網絡可能會導致資金損失 - 謝謝您的反饋 - 成功送出 - 請求支持 - 發送反饋 - 此功能不在展示模式中提供 - 發送電子郵件失敗 - 原因:%s - 無法發送交易 - 此卡不支持Solana網路上的代幣因為韌體限制 - 有困難在掃描卡上嗎? - 此卡不適用於此app - 轉到設置以在 Tangem App 中啟用生物識別身份驗證 - 啟用生物辨識 - 這將刪除所有保存的錢包訪問代碼。對錢包的任何進一步操作都需要提交訪問代碼。 - 移除已保存的卡會刪除所有已保存的錢包及其訪問密碼 - 保存訪問密碼 - 在與您的卡進行交互時,將要求進行生物識別身份驗證,而不是訪問密碼 - 將錢包保存在應用程序中 - 啟用以將所有錢包鏈接到 Tangem 應用程序。解鎖應用程序需要生物識別身份驗證。交易簽名需要輕觸您的 Tangem 卡片 - APP設置 - 請掃描卡片 - 請30秒後重試或刷卡 - 嘗試次數過多 - - %d 卡片 - - 如果您不希望使用此卡重置此錢包中其他卡上的訪問密碼,請禁用此選項。請注意,這也會阻止您重置此卡上的訪問密碼。 - 允許您使用此卡重置此錢包中其他卡上的訪問密碼 - 禁用重置此卡或此錢包中其他卡上的訪問密碼的功能 - 恢復訪問密碼 - 重置 - 您確定要這麼做嗎? - 更改訪問密碼 - 訪問密碼將僅在此卡上更改 - 回復至原廠設置 - 安全模式 - 選定的應用程序保護方法 - 卡片設置 - Tangem 機器人 - 支援 - 請評價 - 發送logs - 請選擇一個操作 - 請評價我們的服務 - 接受 - 添加 - 注意 - 餘額: %s - 生物識別 - 生物 - 購買 - 您尚未授予相機訪問權限,請更改您的隱私設置 - 刪除 - 關閉 - 繼續 - 複製 - 複製地址 - 創造 - 刪除 - 禁用 - 不喜歡 - 完成 - 允許 - 啟用 - 錯誤 - 交易 - 導入 - 喜歡 - - OK - 主卡片 - 拒絕 - 重新命名 - 保存設置 - 搜索 - 搜尋代幣 - 選擇 - 銷售 - 發送 - 伺服器不可用,請稍後在試 - 分享 - 簽署 - 簽署和發送 - 開始 - 提交 - 成功 - 交換 - 費用 - 條款和條件 - 交易 - 我了解 - 無法觸達 - - 已複製代幣地址 - 支持的網路 - 添加代幣 - 代幣地址 - 無效地址 - 衍生路徑錯誤 - 請選擇網路 - 必填 - 十進制數必須是有效整數,不大於 %d - 小數點 - 默認 - BIP44 幣種 - 如 USD Coin - 名稱 - 未選擇 - 網路 - 如 USDC - 代幣符號 - 此代幣/網路已被加入 - 請注意代幣可以被任何人創造。小心詐騙 - 交談 - 訪問密碼 - 在掃描卡之前,您必須提交正確的訪問密碼 - 長按 - 此機制可防止對卡進行鄰近攻擊。它將強制執行命令的接收和執行之間的延遲。 - 密碼 - 在執行任何需要更改卡狀態的命令之前,您必須輸入密碼 - 推薦計畫 - 隱私政策 - %s hashes - 卡號 - 連結更多錢包 - App Currency - 發行人 - 簽署 - 更多 - 檢查您的網路連接或切換到其他網絡 - 服務條款 - 糟糕,當前版本的應用程序無法使用此卡,請檢查更新 - 您使用了另一個錢包中的卡。點按與此錢包關聯的卡片 - 以下信息是可選的。如果不想共享,可以將其刪除 - 告訴我們您缺少哪些功能,我們會盡力幫助您 - 請告訴我們你有什麼卡 - 嗨! 幫助團隊, - 請告訴我們更多有關您的問題。提供任何細節都會帶來幫助 - 我的建議 - 無法掃卡 - 反饋 - Tangem反饋 - 無法發送交易 - 掃描卡片 - 要更改訪問密碼,請完全按照上圖所示連接手機和卡片 - 要更改密碼,請完全按照上圖所示連接手機和卡 - 要創建錢包,請完全按照上圖所示連接手機和卡 - 要重置為出廠設置,請完全按照上圖所示連接手機和卡片 - 點擊掃描 - 要簽名,請完全按照上圖所示連接手機和卡 - 點擊簽名 - 點按卡片 - 內部錯誤:找不到錢包管理器 - 您已更新生物識別登入,掃描您的卡進入 - 管理代幣 - 總餘額 - 該金額不包括您的部分資金 - 要訪問所有的網路您需要掃描卡片 - 掃描卡片 - 代幣 - 您必須設置一個單一的訪問代碼來保護您的所有錢包 - 保護 - 您可以稍後在每張卡上設置單獨的訪問密碼 - 個人化 - 門禁密碼可以用聯動卡恢復,不要把所有卡都放在一個地方 - 恢復 - 選擇您想要的任何單詞、短語或數字作為您的訪問代碼 - 建立訪問密碼 - 重新輸入訪問密碼 - 訪問代碼的長度必須至少為 4 個字符 - 輸入的訪問密碼與初始訪問密碼不匹配 - 您已添加一張備用卡。備份過程完成後,您將無法添加更多備份卡。如果您還有一張卡,請將其添加到備份中。您想繼續備份過程嗎? - 備份過程已部分完成。你現在不能退出 - 餘額 - 添加備用卡 - 掃描卡片 #%d - 立即備份 - 掃描主卡 - 獲取 - 繼續至我的錢包 - 完成備份過程 - 設置PIN碼 - 接收貨幣 - 掃描主卡 - 暫時略過 - 它是如何運作的? - 讓我們生成您卡上的所有密鑰並創建一個安全的錢包 - 創建錢包 - 創造錢包 - 其他選項 - 您的密鑰將在卡內安全生成。沒有種子短語,這意味著沒有人可以導出或竊取它。 - 私下生成密鑰 - 您的卡已激活並可以使用 - 成功! - 這此情況,您必須要重新開始 - 您想要離開啟用程序嗎? - 開始 - 您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎? - PIN 碼 - 連接 - 創建備份 - 閱讀更多關於助記詞的訊息 - 按照下面給出的順序寫下這 12 個單詞,並將它們存放在安全秘密的地方。 - 生成助記詞 - 導入錢包 - 使用助記詞 - Legacy - 那麼,讓我們檢查一下 - 要開始,只需將 wxDAI 獲取到您的錢包 - 這會花上幾秒 - 要開始備份過程,最多可添加兩張備份卡。 - 您可以再添加一張卡或完成備份過程 - 準備編號為 %s 的備份卡 - 掃描主卡以啟動備份過程 - 準備編號為 %s 的主卡 - 恭喜! 您的第一張支付加密卡已啟用! - 您的錢包卡已配置完畢,可以使用了 - 已添加至最大數量。完成備份過程 - 啟用卡片 - 備份卡 #%d - 獲取%s - 獲取中 - 沒有備用卡 - 添加了一張備用卡 - PIN 碼 - 準備好你的卡片 - 添加了兩張備用卡 - 要開始使用,只需為錢包充值任意金額 - 要開始,只需為錢包充值超過 %1$s %2$s - 購買加密貨幣 - 顯示錢包地址 - 啟動錢包 - 結對過程已部分完成。你現在不能退出 - 如果創建錢包的過程以任何方式中斷,您將得重新開始 - 您最多可以額外備份兩張空白的 Tangem冷錢包 - 可以使用其中一張備用卡恢復訪問密碼 - 所有備用卡都可以使用相同的密鑰作為全功能使用 - 您將能夠設置訪問密碼來保護您的錢包 - 備援錢包 - 訪問密碼還原 - 相同的卡片 - 訪問密碼 - 參與 - 無法加載有關推薦計劃的消息。請稍後再試 - 無法加載有關推薦計劃的消息。原因:%s。請稍後再試 - 您的朋友買 - 會得到 - 對於你的朋友在你的 %1$s 網絡地址%2$s上購買的每個錢包 - - 得到 - %s 折扣 - 你的朋友 - 個人促銷碼已複製! - 您的優惠碼 - 以折扣價購買 Tangem 錢包!%s - 推薦您的好友至Tangem - 你接受了 - 點擊此按鈕即表示您接受 - 在推薦計劃 - - %d 錢包 - - 請持有卡片直至操作完成 - 重置卡片 - 我了解執行此操作後,我將無法再訪問當前錢包 - 恢復原廠設置將從所選卡中完全刪除錢包。您將無法恢復當前錢包或使用卡恢復訪問密碼 - 恢復原廠設置將從所選卡中完全刪除錢包並將其從應用程序中刪除。您將無法恢復當前錢包 - 目前不接受俄羅斯銀行卡 - 登錄應用程序並在不掃描卡片的情況下檢查您的資產 - 訪問應用程序 - 允許使用生物辨識 - 將要求生物識別而不是訪問密碼來與您的錢包進行交互 - 訪問密碼 - 您好像禁用了生物識別,有必要保存錢包 - 啟用生物識別授權 - 您想使用生物識別技術嗎? - 請注意,用您的資金進行交易仍需要您的卡片 - 掃卡失敗。再試一次 - 掃描卡片 - 掃描卡片以更改其設置。這些更改只會影響您掃描過的卡,不會影響綁定到您錢包的其他卡。 - 準備好您的卡! - 數量 - 地址 - 地址與錢包地址相同 - 標籤無效。它不會被添加到交易中 - Memo無效。 它不會被添加到交易中 - Tag - Memo - 包含費用 - - 正常 - 優先 - 最大值 - 網路費 - 發送 %s - 總計 - %1$s 和 %2$s 將被發送 - ≈ %1$s (費用包含: %2$s) - %s 將被發送 - 交易已成功簽署並發送至區塊鏈節點。錢包餘額稍後更新 - 無效地址 - 現在購買 - 輸入折扣碼 - Tangem Wallet - 其他付款方式 - 總計 - 安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中 - 創新式的硬體錢包 - 最多 3張實體卡片 到一個錢包 - 最多 - 3張實體卡片 - 到一個錢包 - 更多的安全備份 - 一個冷錢包,可同時將您的比特幣、以太幣和其他幣種-全部存在一張卡中 - 千種虛擬貨幣 - 隨時隨地使用它,不需要接線或電池,只需要你的卡片和手機 - 適合每個人的冷錢包 - 認識Tangem - 交易、購買NFT、自由穿梭於100+去中心化服務 - 兼容Web3.0 - 批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。 - 批准 - 錯誤: %s - 賦予權限 - 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量 - 餘額不足 - 您的 %1$s 錢包中沒有足夠的資金來創建交易。首先為您的 %2$s 錢包充值 - 交易進行中... - 等待中 - 允許 - 賦予權限 - 數量 %s - 花費者 - 您的錢包 - 要繼續,您需要允許 1inch 智能合約使用您的 %s - 在瀏覽器中查看 - 進行中 - 交易 - 交易 %s 至 - 此外,報價包括%s的 Tangem 費用。這有助於我們提供一流的產品 - 其他代幣 - 選擇代幣 - 您的代幣 - 無法使用 - - %d 代幣 - - 隱藏 - 您即將在主屏幕上隱藏此代幣。您可以隨時通過管理代幣頁面將其添加回來。 - 隱藏 %s - 隱藏代幣 - %1$s 代幣是 %2$s 網絡上的主要貨幣,只要列表中還有該網絡上的其他代幣,它就無法被隱藏。 - 無法隱藏 %s - 無費用 - 您還沒有任何交易 - 無法加載交易 - 進行中… - 您掃描了同一張卡片。要創建雙錢包,您需要掃描編號為 %d 的卡 - 你掃描錯了胞胎卡。請嘗試另一個 - 這一個是你手裡拿著的,另一個是編號為 %s 的,這兩張卡都可以用來從這個錢包中提取資金 - 一個錢包,兩張卡片 - 掃描卡片 #%s - 創建錢包 - 掃描 #%s 雙胞胎卡 - 準備卡片 - Tangem Twin - 這個動作是不可逆的。您將無法訪問舊錢包 - 將您的 iPhone 靠近編號為 %s 的雙胞胎卡 - 添加新錢包 - 您確定要刪除此錢包? - 已選擇 %d - 發生錯誤,請掃卡登錄 - 此錢包已保存,您可以再添加一個 - 多幣種 - 錢包名稱 - 重新命名錢包 - 單一幣種 - 我的錢包 - 用 %s 解鎖全部 - 交易記錄 - 區塊鍊無法使用。稍後再試 - 餘額加載中 - 掃描卡片 - 交易進行中 - 檢視餘額 - 動作 - 選擇錢包選項 - 您想要購買或賣出交易貨幣? - 請求籤署消息。%s - Dapp %1$s,請求\n簽署 BNB 交易。\n%2$s - %1$s 的交易訂單\n價格: %2$s\n接收金額:%3$s\n支付數量: %4$s - 交易明細:\n從: %1$s\n到: %2$s\n數量: %3$s - BNB 交易已成功簽署並發送至 Dapp - 剪貼板包含 WalletConnect 代碼。使用複制的值或掃描二維碼 - 請求為 %1$s 創建交易\n%2$s\n\n數量: %3$s\n費用: %4$s\n全部的: %5$s\n餘額: %6$s - 無法交易,無足夠資金 - 未能建立 WalletConnect 連接。請稍後再試 - 並非所有代幣都已添加到您的列表中。請先添加它們,然後重試。缺少標記:\n - 無法簽署消息。請重試 - 無法建立 WalletConnect 連接:超時錯誤。請稍後再試 - 會話請求包含不支持 WalletConnect 連接的區塊鏈。不支持的區塊鏈:\n - 由於技術問題,無法與此 Dapp 建立連接 - 我們遇到了未知錯誤。錯誤信息:%s。如果問題仍然存在-請隨時聯繫我們的客服人員 - 在 Tangem App 中選擇了錯誤的卡 - 無法從 Dapp 數據創建交易。代碼: %s - 我們遇到了未知錯誤。錯誤代碼:%d。如果問題仍然存在-請隨時聯繫我們的支持人員 - 消息已成功簽名並發送至Dapp - 沒有 %s 網路,請先加入後再試一次 - 沒有已連結的WalletConnect - Ooops, 沒有連接 - 從剪貼板貼上 - 給 %1$s 的消息:%2$s - 請求開始會話\n%1$s\n\n網絡: %2$s\n\n網址:%3$s - 無法完成執行,您已經使用此參數建立了 WalletConnect 連接 - 掃描新密碼 - 此卡不能用於建立 WalletConnect 連接 - 不支持此網絡。請選擇其他網絡 - 選擇網路 - Dapp 沒有提供必要的數據來建立 WalletConnect 連接 - 找不到請求的連接 - WalletConnect 連接 - 連結到dApps - WalletConnect - 交易已成功簽署並發送至 Dapp - 交易已成功簽署並發送至區塊鏈 - 未能找到交易哈希 - %s 網路 - 帳號尚未被創造 - 不支持此卡 - 您的 Tangem 卡是為與不同的應用程序一起工作而設計的。請查看卡片上的名稱和說明,並安裝正確的應用程序 - %s市場價格 - 地址已復製到剪貼板 - 無網路 - 接收中 - 來自%s - 發送中 - 至%s - Tangem - 可以更好 - 了解更多 - 了解 - 真的很酷! - 此卡可能是生產樣本或偽造品 - 認證檢查失敗 - 網路無法使用 - Solana 網絡每 2 天收取 %1$s 的費用。無法付此費用的帳戶將從網絡中清除。向您的帳戶存入超過 %2$s 即可免費使用 - 捨棄 - 您有一個備份中斷了,您想繼續嗎? - 是的,恢復 - 捨棄 - 如果您現在要放棄備份,則必須將卡重置為出廠設置才能重新開始 - 恢復備份 - 此動作不可逆 - 使用 %s 登錄 - 掃描卡片 - 使用 %s 或掃描卡片以訪問該應用程序 - 歡迎回來! + 添加自定義代幣 + 管理代幣 + 僅將 %1$s (%2$s) 從 %3$s 網絡發送到此地址。使用其他代幣和網絡可能會導致資金損失 + 謝謝您的反饋 + 成功送出 + 請求支持 + 發送反饋 + 此功能不在展示模式中提供 + 發送電子郵件失敗 + 原因:%s + 無法發送交易 + 此卡不支持%1$s網路上的代幣因為韌體限制 + 有困難在掃描卡上嗎? + 此卡不適用於此app + 轉到設置以在 Tangem App 中啟用生物識別身份驗證 + 啟用生物辨識 + 這將刪除所有保存的錢包訪問代碼。對錢包的任何進一步操作都需要提交訪問代碼。 + 移除已保存的卡會刪除所有已保存的錢包及其訪問密碼 + 保存訪問密碼 + 在與您的卡進行交互時,將要求進行生物識別身份驗證,而不是訪問密碼 + 將錢包保存在應用程序中 + 啟用以將所有錢包鏈接到 Tangem 應用程序。解鎖應用程序需要生物識別身份驗證。交易簽名需要輕觸您的 Tangem 卡片 + APP設置 + 請掃描卡片 + 請30秒後重試或刷卡 + 嘗試次數過多 + + %d 卡片 + + 如果您不希望使用此卡重置此錢包中其他卡上的訪問密碼,請禁用此選項。請注意,這也會阻止您重置此卡上的訪問密碼。 + 允許您使用此卡重置此錢包中其他卡上的訪問密碼 + 禁用重置此卡或此錢包中其他卡上的訪問密碼的功能 + 恢復訪問密碼 + 重置 + 您確定要這麼做嗎? + 更改訪問密碼 + 訪問密碼將僅在此卡上更改 + 回復至原廠設置 + 安全模式 + 選定的應用程序保護方法 + 卡片設置 + Tangem 機器人 + 支援 + 發送logs + 接受 + 添加 + 注意 + 餘額: %s + 餘額 + 生物識別 + 生物 + 購買 + 您尚未授予相機訪問權限,請更改您的隱私設置 + 刪除 + 關閉 + 繼續 + 複製 + 複製地址 + 創造 + 刪除 + 禁用 + 不喜歡 + 完成 + 允許 + 啟用 + 錯誤 + 交易 + 費用 + 導入 + 喜歡 + 網路費 + + OK + 主卡片 + 拒絕 + 重新命名 + 保存設置 + 搜索 + 搜尋代幣 + 選擇 + 銷售 + 發送 + 伺服器不可用,請稍後在試 + 分享 + 簽署 + 簽署和發送 + 開始 + 提交 + 成功 + 交換 + 條款和條件 + 交易 + 我了解 + 無法觸達 + + 已複製代幣地址 + 支持的網路 + 添加代幣 + 代幣地址 + 無效地址 + 衍生路徑錯誤 + 請選擇網路 + 必填 + 小數點 + 默認 + BIP44 幣種 + 如 USD Coin + 名稱 + 未選擇 + 網路 + 如 USDC + 代幣符號 + 此代幣/網路已被加入 + 請注意代幣可以被任何人創造。小心詐騙 + 交談 + 訪問密碼 + 在掃描卡之前,您必須提交正確的訪問密碼 + 長按 + 此機制可防止對卡進行鄰近攻擊。它將強制執行命令的接收和執行之間的延遲。 + 密碼 + 在執行任何需要更改卡狀態的命令之前,您必須輸入密碼 + 推薦計畫 + 隱私政策 + %s hashes + 卡號 + 連結更多錢包 + App Currency + 發行人 + 簽署 + 更多 + 檢查您的網路連接或切換到其他網絡 + 服務條款 + 糟糕,當前版本的應用程序無法使用此卡,請檢查更新 + 您使用了另一個錢包中的卡。點按與此錢包關聯的卡片 + 以下信息是可選的。如果不想共享,可以將其刪除 + 告訴我們您缺少哪些功能,我們會盡力幫助您 + 請告訴我們你有什麼卡 + 嗨! 幫助團隊, + 請告訴我們更多有關您的問題。提供任何細節都會帶來幫助 + 我的建議 + 無法掃卡 + 反饋 + Tangem反饋 + 無法發送交易 + 掃描卡片 + 要更改訪問密碼,請完全按照上圖所示連接手機和卡片 + 要更改密碼,請完全按照上圖所示連接手機和卡 + 要創建錢包,請完全按照上圖所示連接手機和卡 + 要重置為出廠設置,請完全按照上圖所示連接手機和卡片 + 點擊掃描 + 要簽名,請完全按照上圖所示連接手機和卡 + 點擊簽名 + 點按卡片 + 內部錯誤:找不到錢包管理器 + 您已更新生物識別登入,掃描您的卡進入 + 管理代幣 + 總餘額 + 該金額不包括您的部分資金 + 要訪問所有的網路您需要掃描卡片 + 掃描卡片 + 代幣 + 您必須設置一個單一的訪問代碼來保護您的所有錢包 + 保護 + 您可以稍後在每張卡上設置單獨的訪問密碼 + 個人化 + 門禁密碼可以用聯動卡恢復,不要把所有卡都放在一個地方 + 恢復 + 選擇您想要的任何單詞、短語或數字作為您的訪問代碼 + 建立訪問密碼 + 重新輸入訪問密碼 + 訪問代碼的長度必須至少為 4 個字符 + 輸入的訪問密碼與初始訪問密碼不匹配 + 您已添加一張備用卡。備份過程完成後,您將無法添加更多備份卡。如果您還有一張卡,請將其添加到備份中。您想繼續備份過程嗎? + 備份過程已部分完成。你現在不能退出 + 添加備用卡 + 掃描卡片 #%d + 立即備份 + 掃描主卡 + 獲取 + 繼續至我的錢包 + 完成備份過程 + 設置PIN碼 + 接收貨幣 + 掃描主卡 + 暫時略過 + 它是如何運作的? + 讓我們生成您卡上的所有密鑰並創建一個安全的錢包 + 創建錢包 + 創造錢包 + 其他選項 + 您的密鑰將在卡內安全生成。沒有種子短語,這意味著沒有人可以導出或竊取它。 + 私下生成密鑰 + 您的卡已激活並可以使用 + 成功! + 這此情況,您必須要重新開始 + 您想要離開啟用程序嗎? + 開始 + 您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎? + PIN 碼 + 連接 + 創建備份 + 閱讀更多關於助記詞的訊息 + 按照下面給出的順序寫下這 12 個單詞,並將它們存放在安全秘密的地方。 + 生成助記詞 + 導入錢包 + 使用助記詞 + Legacy + 那麼,讓我們檢查一下 + 要開始,只需將 wxDAI 獲取到您的錢包 + 這會花上幾秒 + 要開始備份過程,最多可添加兩張備份卡。 + 您可以再添加一張卡或完成備份過程 + 準備編號為 %s 的備份卡 + 掃描主卡以啟動備份過程 + 準備編號為 %s 的主卡 + 恭喜! 您的第一張支付加密卡已啟用! + 您的錢包卡已配置完畢,可以使用了 + 已添加至最大數量。完成備份過程 + 啟用卡片 + 備份卡 #%d + 獲取%s + 獲取中 + 沒有備用卡 + 添加了一張備用卡 + PIN 碼 + 準備好你的卡片 + 添加了兩張備用卡 + 要開始使用,只需為錢包充值任意金額 + 要開始,只需為錢包充值超過 %1$s %2$s + 購買加密貨幣 + 顯示錢包地址 + 啟動錢包 + 結對過程已部分完成。你現在不能退出 + 如果創建錢包的過程以任何方式中斷,您將得重新開始 + 您最多可以額外備份兩張空白的 Tangem冷錢包 + 可以使用其中一張備用卡恢復訪問密碼 + 所有備用卡都可以使用相同的密鑰作為全功能使用 + 您將能夠設置訪問密碼來保護您的錢包 + 備援錢包 + 訪問密碼還原 + 相同的卡片 + 訪問密碼 + 參與 + 無法加載有關推薦計劃的消息。請稍後再試 + 無法加載有關推薦計劃的消息。原因:%s。請稍後再試 + 您的朋友買 + 會得到 + 對於你的朋友在你的 %1$s 網絡地址%2$s上購買的每個錢包 + + 得到 + %s 折扣 + 你的朋友 + 個人促銷碼已複製! + 您的優惠碼 + 以折扣價購買 Tangem 錢包!%s + 推薦您的好友至Tangem + 你接受了 + 點擊此按鈕即表示您接受 + 在推薦計劃 + + %d 錢包 + + 請持有卡片直至操作完成 + 重置卡片 + 我了解執行此操作後,我將無法再訪問當前錢包 + 恢復原廠設置將從所選卡中完全刪除錢包。您將無法恢復當前錢包或使用卡恢復訪問密碼 + 恢復原廠設置將從所選卡中完全刪除錢包並將其從應用程序中刪除。您將無法恢復當前錢包 + 目前不接受俄羅斯銀行卡 + 登錄應用程序並在不掃描卡片的情況下檢查您的資產 + 訪問應用程序 + 允許使用生物辨識 + 將要求生物識別而不是訪問密碼來與您的錢包進行交互 + 訪問密碼 + 您好像禁用了生物識別,有必要保存錢包 + 啟用生物識別授權 + 您想使用生物識別技術嗎? + 請注意,用您的資金進行交易仍需要您的卡片 + 掃卡失敗。再試一次 + 掃描卡片 + 掃描卡片以更改其設置。這些更改只會影響您掃描過的卡,不會影響綁定到您錢包的其他卡。 + 準備好您的卡! + 數量 + 地址 + 地址與錢包地址相同 + 標籤無效。它不會被添加到交易中 + Memo無效。 它不會被添加到交易中 + Tag + Memo + 包含費用 + + 正常 + 優先 + 最大值 + 發送 %s + 總計 + %1$s 和 %2$s 將被發送 + ≈ %1$s (費用包含: %2$s) + %s 將被發送 + 交易已成功簽署並發送至區塊鏈節點。錢包餘額稍後更新 + 無效地址 + 現在購買 + 輸入折扣碼 + Tangem Wallet + 其他付款方式 + 安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中 + 創新式的硬體錢包 + 最多 3張實體卡片 到一個錢包 + 最多 + 3張實體卡片 + 到一個錢包 + 更多的安全備份 + 一個冷錢包,可同時將您的比特幣、以太幣和其他幣種-全部存在一張卡中 + 千種虛擬貨幣 + 隨時隨地使用它,不需要接線或電池,只需要你的卡片和手機 + 適合每個人的冷錢包 + 認識Tangem + 交易、購買NFT、自由穿梭於100+去中心化服務 + 兼容Web3.0 + 批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。 + 批准 + 錯誤: %s + 賦予權限 + 價格影響高! + 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量 + 餘額不足 + 您的 %1$s 錢包中沒有足夠的資金來創建交易。首先為您的 %2$s 錢包充值 + 交易進行中... + 等待中 + 允許 + 賦予權限 + 數量 %s + 花費者 + 您的錢包 + 要繼續,您需要允許 1inch 智能合約使用您的 %s + 在瀏覽器中查看 + 進行中 + 交易 + 交易 %s 至 + 此外,報價包括%s的 Tangem 費用。這有助於我們提供一流的產品 + 其他代幣 + 選擇代幣 + 您的代幣 + 無法使用 + + %d 代幣 + + 隱藏 + 您即將在主屏幕上隱藏此代幣。您可以隨時通過管理代幣頁面將其添加回來。 + 隱藏 %s + 隱藏代幣 + %1$s 代幣是 %2$s 網絡上的主要貨幣,只要列表中還有該網絡上的其他代幣,它就無法被隱藏。 + 無法隱藏 %s + 無費用 + 您還沒有任何交易 + 無法加載交易 + 進行中… + 您掃描了同一張卡片。要創建雙錢包,您需要掃描編號為 %d 的卡 + 你掃描錯了胞胎卡。請嘗試另一個 + 這一個是你手裡拿著的,另一個是編號為 %s 的,這兩張卡都可以用來從這個錢包中提取資金 + 一個錢包,兩張卡片 + 掃描卡片 #%s + 創建錢包 + 掃描 #%s 雙胞胎卡 + 準備卡片 + Tangem Twin + 這個動作是不可逆的。您將無法訪問舊錢包 + 將您的 iPhone 靠近編號為 %s 的雙胞胎卡 + 添加新錢包 + 您確定要刪除此錢包? + 已選擇 %d + 發生錯誤,請掃卡登錄 + 此錢包已保存,您可以再添加一個 + 多幣種 + 錢包名稱 + 重新命名錢包 + 單一幣種 + 我的錢包 + 用 %s 解鎖全部 + 交易記錄 + 區塊鍊無法使用。稍後再試 + 餘額加載中 + 掃描卡片 + 交易進行中 + 檢視餘額 + 動作 + 選擇錢包選項 + 您想要購買或賣出交易貨幣? + 請求籤署消息。%s + Dapp %1$s,請求\n簽署 BNB 交易。\n%2$s + %1$s 的交易訂單\n價格: %2$s\n接收金額:%3$s\n支付數量: %4$s + 交易明細:\n從: %1$s\n到: %2$s\n數量: %3$s + BNB 交易已成功簽署並發送至 Dapp + 剪貼板包含 WalletConnect 代碼。使用複制的值或掃描二維碼 + 請求為 %1$s 創建交易\n%2$s\n\n數量: %3$s\n費用: %4$s\n全部的: %5$s\n餘額: %6$s + 無法交易,無足夠資金 + 未能建立 WalletConnect 連接。請稍後再試 + 並非所有代幣都已添加到您的列表中。請先添加它們,然後重試。缺少標記:\n + 無法簽署消息。請重試 + 無法建立 WalletConnect 連接:超時錯誤。請稍後再試 + 會話請求包含不支持 WalletConnect 連接的區塊鏈。不支持的區塊鏈:\n + 由於技術問題,無法與此 Dapp 建立連接 + 我們遇到了未知錯誤。錯誤信息:%s。如果問題仍然存在-請隨時聯繫我們的客服人員 + 在 Tangem App 中選擇了錯誤的卡 + 無法從 Dapp 數據創建交易。代碼: %s + 我們遇到了未知錯誤。錯誤代碼:%d。如果問題仍然存在-請隨時聯繫我們的支持人員 + 消息已成功簽名並發送至Dapp + 沒有 %s 網路,請先加入後再試一次 + 沒有已連結的WalletConnect + Ooops, 沒有連接 + 從剪貼板貼上 + 給 %1$s 的消息:%2$s + 請求開始會話\n%1$s\n\n網絡: %2$s\n\n網址:%3$s + 無法完成執行,您已經使用此參數建立了 WalletConnect 連接 + 掃描新密碼 + 此卡不能用於建立 WalletConnect 連接 + 不支持此網絡。請選擇其他網絡 + 選擇網路 + Dapp 沒有提供必要的數據來建立 WalletConnect 連接 + 找不到請求的連接 + WalletConnect 連接 + 連結到Dapps + WalletConnect + 交易已成功簽署並發送至 Dapp + 交易已成功簽署並發送至區塊鏈 + 未能找到交易哈希 + %s 網路 + 帳號尚未被創造 + 不支持此卡 + 您的 Tangem 卡是為與不同的應用程序一起工作而設計的。請查看卡片上的名稱和說明,並安裝正確的應用程序 + %s市場價格 + 地址已復製到剪貼板 + 無網路 + 接收中 + 來自%s + 發送中 + 至%s + Tangem + 可以更好 + 了解更多 + 了解 + 真的很酷! + 此卡可能是生產樣本或偽造品 + 認證檢查失敗 + 網路無法使用 + Solana 網絡每 2 天收取 %1$s 的費用。無法付此費用的帳戶將從網絡中清除。向您的帳戶存入超過 %2$s 即可免費使用 + 捨棄 + 您有一個備份中斷了,您想繼續嗎? + 是的,恢復 + 捨棄 + 如果您現在要放棄備份,則必須將卡重置為出廠設置才能重新開始 + 恢復備份 + 此動作不可逆 + 使用 %s 登錄 + 掃描卡片 + 使用 %s 或掃描卡片以訪問該應用程序 + 歡迎回來! diff --git a/core/res/src/main/res/values/strings-blockchain.xml b/core/res/src/main/res/values/strings-blockchain.xml index 2aeb56c771..ef4e2b2edb 100644 --- a/core/res/src/main/res/values/strings-blockchain.xml +++ b/core/res/src/main/res/values/strings-blockchain.xml @@ -6,10 +6,9 @@ Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. Not enough funds for the transaction. Please top up your account. An error occurred. Code: %s. - Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount. - To create account send funds to this address To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely Destination account is not active. Send %s or more to activate the account. + To create account send funds to this address Minimum amount is %s Change is too small Invalid Fee diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index cada81a5ff..dc081f161b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1,762 +1,780 @@ - Add custom token - Manage tokens - Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. - Thank you for your feedback - Sent successfully - Request support - Send feedback - This feature is disabled in Demo mode - Failed to send the email - Reason: %s - Can\'t send a transaction - The selected does not support the %1$s network - To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset. - Tokens in %1$s network are not supported by this card due to firmware limitation. - Are you having difficulty scanning your card? - This card is not designed to work with this app - Go to settings to enable biometric authentication in the Tangem App - Enable biometric authentication - This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. - Removing the saved card deletes all the saved wallets and their access codes from the app. - Save Access Code - Biometric authentication will be requested instead of the access code for interactions with your card. - Keep the wallet in the app - Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. - Dark - Light - System default - Theme - App Settings - To hide or show your balances, simply flip your device screen down, or switch it off in Settings - Don\'t show again - Got it - Balances are hidden - Please scan the card - Please try again in 30 seconds or scan the card - Too many attempts - You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. - Start backup process - - %d card - %d cards - - Disable this option if you don\'t want this card to be used to reset access codes on other cards in this wallet. Please note that this will also prevent you from resetting the access code on this card. - Allows you to use this card to reset access code on other cards in this wallet - Disable the ability to reset the access code on this card or other cards in this wallet - Access code recovery - Reset - Are you sure you want to do this? - Change Access Code - Access code will be changed on this card only - Reset to Factory Settings - Security Mode - Selected application protection method - Card Settings - Tangem Bot - Support - Send logs - Accept - Access denied - Add - Apply - Approval - Attention - Balance: %s - Balance - biometric authentication - biometrics - Buy - Go to %1$s - You have not given access to your camera, please adjust your privacy settings - Cancel - Close - Continue - Copy - Copy address - Create - Delete - Disabled - Dislike - Done - Enable - Enabled - Error - Exchange - Explore - Explore history - Explore transaction history - Explorer - Fee - Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s - Custom - Fast - Market - Slow - Speed and fee - Go to provider - Get addresses - Import - Learn & Earn - Like - Locked - Main network - Next - No - No address - No data - OK - Primary Card - Paste - Read more - Receive - Reject - Reload - Rename - Save changes - Search - Search tokens - Seed phrase - Select action - Sell - Send - The server is not available, please try again later - Share - Sign - Sign and send - Stake - Start - Submit - Success - Support - Swap - terms and conditions - Transaction failed - Transactions - Transfer - I understand - Unreachable - There was an error. Please try again. - Yes - Contract address copied! - Available networks - Add token - Contract address - Contract address is invalid - Derivation path is invalid - Please select the network - Required field - Decimal number must be a valid integer, no higher than %d - Custom derivation - E. g. m/00\'/0000\'/0\'/0/0 - Enter custom derivation - Decimals - Derivation Path - Default - BIP44 coin type - The derivation path you\'ve entered is not valid - E.g. USD Coin - Name - Not selected - Network - Token network - You can manually add a token that is not natively supported by Tangem - E.g. USDC - Symbol - Token symbol - This token/network has already been added to your list - Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing. - Be aware of adding scam tokens, they can cost nothing - Note that tokens can be created by anyone - Chat - Access code - You will have to submit the correct access code before scanning the card - Long Tap - This mechanism protects against proximity attacks on a card. It will enforce a delay between reception and execution of a command. - Passcode - Before executing any command entailing a change of the card state, you will have to enter the passcode. - Referral program - Flip your device screen down to quickly hide and show balances - Privacy policy - %s hashes - Card ID - Contact to Support - Link More Cards - App Currency - Flip-to-Hide Balances - Issuer - Signed - Details - Check your internet connection or switch to a different network - Terms of Service - Oops, the current version of the application is not ready to work with this card, please check for updates. - You have used a card from another wallet. Tap the card associated with this wallet - My tokens - You haven\'t added any tokens yet. Add tokens via Market to swap - No tokens found. Please try another request - Cannot be swapped for %s - Additionally, the network fee for sending the exchanged funds back to your address is included in the rate - Status - Providers facilitate transactions, ensuring smooth and efficient token swaps - Choose provider - More providers are coming soon.\nStay tuned! - Estimated amount - Exchange by %s - Provided by - Visit provider’s website to refund your money - Operation failed by provider - Visit provider’s website for verification - KYC verification required by provider - Canceled - Confirmed - Confirming - Confirming… - Exchanged - Exchanging - Exchanging… - Failed - Deposit received - Awaiting deposit - Awaiting deposit… - Refunded - Sending to you - Sending to you… - Sent - Provider-sourced data. Estimated amount subject to change due to market conditions. - Exchange status - Verified - Verification required - List of all tokens added to your wallet - Fetching best rates... - Floating rate - Go to provider - By using swap functionality, you agree with provider’s %s - By using swap functionality, you agree with provider’s %1$s and %2$s - Privacy Policy - Provider - Best rate - Available from %s - Available up to %s - Unavailable for this pair - Permission Required - Terms of Use - The following information is optional. You can erase it if you don\'t want to share it. - Tell us what functions you are missing, and we will try to help you. - Please tell us what card do you have - Hi support team, - Please tell us more about your issue. Every small detail can help. - My suggestions - Can\'t scan a card - Feedback - Tangem feedback - Can\'t send a transaction - Order card - Scan card - To change the access code tap the card as shown above and do not remove until the end of the operation - To change the passcode tap the card as shown above and do not remove until the end of the operation - To create the wallet tap the card as shown above and do not remove until the end of the operation - To reset to factory settings tap the card as shown above and do not remove until the end of the operation - Tap to scan - To sign tap the card as shown above and do not remove until the end of the operation - Tap to sign - Tap the card - Internal error: wallet manager not found - You have updated biometrics, scan your card to enter - To begin tracking your crypto assets and transactions, add tokens - You have completed all of the lessons, and are now eligible to receive your 1INCH tokens - - Complete three lessons and receive %d 1INCH token to your wallet - Complete three lessons and receive %d 1INCH tokens to your wallet - - Manage tokens - Total balance - The amount does not include some of your funds - 1INCH tokens will be credited to your %s wallet address within 48 hours - To access all the networks you need to scan the card - Scan your card - Enjoy %1$s service fees on swaps via Changelly from February %2$s-%3$s - Swap with Changelly, %s fees - Tokens - Add - Edit - Couldn’t find this token, you can add it manually - Coin market cap - Blockchain the cryptocurrency was initially created - Native network - Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk. - Not original or primary blockchain the token is hosted - Non-native networks - Networks - Choose networks - Wallet - Couldn’t find this token, you can add it manually - - %1$d of %2$d wallet - %1$d of %2$d wallets - - e.g. BTC I trust, hodl I must - Coin market cap - The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it. - Upvote - Choose wallet - The wallet doesn\'t support more than one network - You have to set up a single access code to protect all your wallets - Protect - You can set up an individual access code on each card later - Personalize - The access code can be restored with a linked card, don\'t keep all cards in one place - Restore - Choose any word, phrase, or number you want as your access code - Create Access Code - Re-enter your Access Code - Access code must be at least 4 characters long - Entered access code didn\'t match the initial access code - Please repeat the operation. The card will be reset to factory settings. - Activation error - You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process? - The backup process is partly complete. You can\'t exit it now. - Add a backup card - Scan the card #%d - Backup now - Scan the primary card - Claim - Continue to my wallet - Finalize the backup - Set PIN code - Receive crypto - Scan primary card - Skip for later - How does it work? - Let\'s generate all the keys on your card and create a secure wallet - Create wallet - Create a wallet - Other options - Your keys will be securely generated inside the card. There is no seed phrase, which means nobody can export or steal it. - Generate keys privately - Your card is activated and ready to be used - Success! - In this case, you will need to start from the beginning. - Do you want to exit the activation process? - Getting started - Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet? - Pin code - Connect - Creating a backup - Read more about seed phrase - Write these 12 words down in the order given below and store them in a safe and secret place. - Your seed phrase - To import your wallet, enter your seed phrase in the field below - Generate seed phrase - Import wallet - A seed phrase is a series of words that allows you to recover your wallet. Unlike the keys generated by the card, seed phrases are unprotected and can be copied and stolen. Use this option at your own risk. - Use seed phrase - Invalid seed phrase. Please check the word order. - Invalid seed phrase. Please check your spelling. - Legacy - To check whether you’ve written down your seed phrase correctly, please enter the 2nd, 7th and 11th words - So, let’s check - To get started, simply claim wxDAI to your wallet - It will take a few seconds - To start the backup process add up to two backup cards. - You can add one more card or finalize the backup process - Prepare the backup card with number %s - Scan the primary card to start the backup process. - Prepare the primary card with number %s - Congratulations! Your first payment crypto card has been activated! - Your wallet card is configured and ready for use. - Max number of cards added. Finalize the backup process. - Activating card - Backup card #%d - Claim %s - Claiming - No backup cards - One backup card added - PIN Code - Prepare your card - Two backup cards added - To get started, simply top up the wallet with any amount - To get started, simply top up the wallet with more than %1$s %2$s - Buy crypto - Show the wallet\'s address - Activate a wallet - The twinning process is partly complete. You can\'t exit it now. - If the process of creating the wallet gets interrupted in any way, you\'ll have to start over - You can backup your keys up to two other blank Tangem Wallet cards. - Access code can be restored with one of backup cards. - All the backup cards can be used as full-functional with the identical keys. - You will be able to set an access code to protect your wallets. - Backup wallet - Access code restore - Identical cards - Access code - Group - By balance - Organize tokens - Ungroup - Select from the gallery - Settings - Camera access denied - You have not given access to your camera - %1$s %2$s address on %3$s network - %1$s (%2$s) on %3$s network - Send only %s to this address. Sending any other currency will result in its irreversible loss. - Participate - Failed to load the information about the referral program. Please try again later. - Failed to load the information about the referral program. Error code: %s. Please try again later. - Upcoming payments - Your friends bought - Less - More - No upcoming payments - - for %d wallet - for %d wallets - - Will get ^^%1$s^^ for each wallet bought by your friend on your %2$s network address %3$s ^^30 days after^^ that - Will get - for each wallet bought by your friend on your %1$s network address%2$s - You - Will get a - when buying a wallet on tangem.com - %s discount - Your friend - Personal code copied! - Your personal code - Buy Tangem Wallet with discount!\n%s - Refer your friends to Tangem - You\'ve accepted - By tapping this button you accept - of the referral program - - %d wallet - %d wallets - - Please hold the card until the operation complete - Reset the Card - I understand that after performing this action, I will no longer have access to the current wallet - I realize that I can\'t use this card to recover my access code on the other cards of the current wallet - Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. - Factory Reset will completely delete the wallet from the selected card and remove it from the app. You will not be able to restore the current wallet. - Do you have a bank card from another country and a residence permit or registration outside the Russian Federation? - Russian bank cards are not currently accepted - Log into the app and check your balance without scanning the card - Access the app - Allow to use biometrics - Biometrics will be requested instead of the access code for interactions with your wallet - Access code - It looks like you have biometric authentication disabled, it is necessary to save wallets - Enable biometric authorization - Would you like to use biometrics? - Note that making a transaction with your funds will still require your card - Scan card failed. Try again - Scan Card - Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet. - Get your card ready! - Already included in the entered address - Amount - The transaction is not completed - Reason: %1$s\nCode: %2$s - Not enough funds to cover the network commission. Subtract the commission from the amount sent? - Subtract - Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? - Confirm - %1$s at %2$s - Address - Destination Tag - Enter address - Address is the same as wallet address - Invalid Tag. It won\'t be added to the transaction. - Invalid Memo. It won\'t be added to the transaction. - Tag - Memo - Include fee - Low - Normal - Priority - Check your network connection - Network fee info unreachable - From - Gas limit - Gas Limit is auto-calculated; raise it during network congestion - Gas price - Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed - Max - Maximum amount - Max fee - Maximum fee amount - Numbers only for Destination Tag - Network fee - Sending amount will be reduced by %1$s to cover the selected commission level. The recipient will get %2$s. - Network fee coverage - Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance - Total exceeds balance - Fee is increased - The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01. - Custom fee is high - The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. - The included commission exceeds the transfer amount, leading to a negative value - Invalid amount - The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s. - Target account is not created. Please change the amount to send. - The amount to send must be at least %1$s - Kindly be aware that your transaction may experience delays under specific fee settings - Transaction delays are possible - Transaction limitation - Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. - Existential deposit - The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s. - Reduce by %s - Reduce to %s - Optional - Please align your QR code with the square to scan it. Ensure you scan %s network address. - Recent - Recipient - Send to - Not a valid address - Ensure that you are sending funds to an %s wallet address. Errors may result in the loss of your tokens - A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds - My wallets - Sending %s - Total - %1$s and %2$s will be sent - ≈ %1$s (inc. fee: %2$s) - %s will be sent - Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while - Invalid address - Transaction sent - %s (%s) - Buy now - I have a promo code… - Tangem Wallet - Other payment methods - Pre-order now - Total - Store your crypto assets secure while keeping private keys contained in your card - Revolutionary Hardware Wallet - Up to 3 physical cards to one wallet - Up to - 3 physical cards - to one wallet - Ultra Secure Backup - A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card - Thousands of Currencies - Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. - The Wallet for Everyone - Take three lessons, get a discount on your Tangem Wallet, and receive 1INCH tokens to your wallet - Learn - Meet Tangem - Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services - Web 3.0 Compatible - All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval. - Approve - Error: %s - You swap - Give Permission - Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. - Insufficient funds - Fees - The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address. - The amount includes the service provider\'s fee. - Sending amount will be reduced to cover the selected fee level - Insufficient funds in your %1$s wallet to cover fees. Top up your %2$s wallet first. - Transaction in progress... - Waiting - Approve - Current transaction - The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. - Give Permission - Specify the approve limit for the selected token - Amount %s - Spender - Your wallet - To continue, grant 1inch smart contracts permission to use your %s - Unlimited - View in Explorer - In progress - Swap - Swap %s for - Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product. - You receive - Other tokens - Choose token - Your tokens - not available - Balances hidden - Balances shown - Undo - - %d token - %d tokens - - Hide - You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. - Hide %s - Hide token - %1$s token in %%image%% %2$s network - The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list. - Unable to hide %s - No rate - Exchange this token for another at %1$s service fees from February %2$s-%3$s. - Swap with Changelly, %s fees - Swap now - contract: %s - You don\'t have any transactions yet - Failed to load transaction history.\nClick on reload button to update the information. - Multiple addresses - Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer. - Operation - from: %s - to: %s - In progress… - You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d - You\'ve scanned wrong twin card. Please try another one - This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. - One wallet. Two cards. - Scan the card #%s - Creating wallet - Scan the #%s twin card - Preparing card - Tangem Twin - This action is irreversible. You will not have access to the old wallet. - Tap the twin card with number %s and do not remove until the end of the operation - Use %s or scan a card to have an access to your wallet - Add new wallet - Are you sure you want to delete this wallet? - %d selected - An error has occurred, please scan your card to log in - This wallet has already been saved, you can add another one - Multi-currency - Wallet name - Rename Wallet - Single-currency - My Wallets - Unlock all - Unlock all with %s - Transaction history - Blockchain is unreachable. Try later - Balance is loading… - Scan the card - Transaction is in progress… - Verified Balance - Actions - Choose wallet option - Do you want to buy or sell crypto? - Requesting to sign a message.\n\n%s - Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s - Trade order for %1$s\nPrice: %2$s\nAmount to receive: %3$s\nAmount to pay: %4$s - Transaction details:\nFrom: %1$s\nTo: %2$s\nAmount: %3$s - The BNB transaction has been successfully signed and sent to the Dapp - Clipboard contain WalletConnect code. Use copied value or scan QR-code - Request to create transaction for %1$s\n%2$s\n\nAmount: %3$s\nFee: %4$s\nTotal: %5$s\nBalance: %6$s - Can\'t send transaction. Not enough funds. - Failed to establish WalletConnect session. Please, try again later. - Not all tokens were added to your list. Please add them first and try again. Missing tokens:\n - Failed to sign message.\nPlease, try again - Failed to establish WalletConnect session: timeout error. Please, try again later. - Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n - Connection with this Dapp cannot be established due to its technical implementation. - We\'ve encountered unknown error. Error message: %s. If the problem persists — feel free to contact our support - Wrong card selected in Tangem App - Failed to create transaction from Dapp data. Code: %s - We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support - The message has been successfully signed and sent to the Dapp - %s network not found. Please, add it first and try again. - No opened WalletConnect sessions - Ooops. No Sessions. - Failed to pairing WalletConnect session: %1$s - Paste from clipboard - Message for %1$s:\n%2$s - Request to start a session for\n%1$s\n\nNETWORK: %2$s\n\nURL: %3$s - The operation couldn\'t be completed.\n\nYou have already established a WalletConnect session with this parameters. - Scan new code - This card can\'t be used to establish WalletConnect session - This network is not supported. Please select another network. - Select network - Dapp didn\'t provide essential data to establish WalletConnect session - Failed to find session for request - WalletConnect Sessions - Connect to dApps - WalletConnect - The transaction has been successfully signed and sent to the Dapp - The transaction has been succesfully signed and sent to the blockchain - Failed to find transaction hash - %s network - Account is not created - This card is not supported - Your Tangem card was made to work with a different application. Please see the name and instructions on your card, and install the correct app - %s Market Price - last 24h - %s network - Address was copied to clipboard - No internet connection - Receiving - from %s - Sending - to %s - Tangem - Use %s or scan a card to unlock access to your wallet - Could be better - Learn more - Like it - Ok, Got it! - Really cool! - Refresh - You are currently in the Demo mode - Demo mode active - The card you scanned is a developer card. Do not use it to create your wallet. - Not for users! - %1$s network requires an Existential Deposit. If your account drops below %2$s, it will be deactivated, and any remaining funds will be destroyed. - Network requires Existential Deposit - Swap will be available after the %s transaction is complete - You have active transaction - You do not have any %s exchangeable coins in your list - No available tokens to swap - To make a transaction you need to deposit some %1$s %2$s - Unable to cover %s fee - Service temporarily unavailable - Please change the amount to swap - The amount to swap must be at least %s - The amount of tokens to be swapped must not exceed %s - This card might be a production sample or counterfeit - Authenticity check failed - Only %s signatures are left on this card. You must withdraw all of your funds. - Low signature count - Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds. - - Use your card to get an address for %d network - Use your card to get an addresses for %d networks - - Some addresses are missing - The network is currently unreachable. Please try again later. - Network is unreachable - Top up your wallet - Your wallet hasn\'t been backed up. Carry out this procedure to protect your assets now. - Missing backup - This card has been previously used for transactions. If received from an untrusted source, consider withdrawing all funds. If it\'s your card, no action is required. - Card has already signed transactions - Device incompatibility detected - Your review keeps us motivated to make Tangem Wallet even better - Enjoying Tangem? - Network rent fee required - %1$s is an asset in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee. - Insufficient %1$s to cover network fee - Sending funds will be available once the %s transaction is complete - Transaction pending - Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free. - Some networks currently are unreachable. Please try again later. - Some networks are unreachable - This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes. - For testing purposes only - Discard - You have an interrupted backup. Do you want to resume? - Yes, resume - Discard - If you will discard the backup now, then you will have to reset the cards to factory settings to start over again - Resume backup - This is an irreversible action - Log in with %s - Scan card - Use %s or scan a card to access the app - Welcome back! + Add custom token + Manage tokens + Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. + Thank you for your feedback + Sent successfully + Request support + Send feedback + This feature is disabled in Demo mode + Failed to send the email + Reason: %s + Can\'t send a transaction + The selected does not support the %1$s network + To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset. + Tokens in %1$s network are not supported by this card due to firmware limitation. + Are you having difficulty scanning your card? + This card is not designed to work with this app + Go to settings to enable biometric authentication in the Tangem App + Enable biometric authentication + This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. + Removing the saved card deletes all the saved wallets and their access codes from the app. + Save Access Code + Biometric authentication will be requested instead of the access code for interactions with your card. + Keep the wallet in the app + Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. + Dark + Light + System default + Theme + App Settings + To hide or show your balances, simply flip your device screen down, or switch it off in Settings + Don\'t show again + Got it + Balances are hidden + Please scan the card + Please try again in 30 seconds or scan the card + Too many attempts + You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. + Start backup process + + %d card + %d cards + + Disable this option if you don\'t want this card to be used to reset access codes on other cards in this wallet. Please note that this will also prevent you from resetting the access code on this card. + Allows you to use this card to reset access code on other cards in this wallet + Disable the ability to reset the access code on this card or other cards in this wallet + Access code recovery + Reset + Are you sure you want to do this? + Change Access Code + Access code will be changed on this card only + Reset to Factory Settings + Security Mode + Selected application protection method + Card Settings + Tangem Bot + Support + Send logs + Accept + Access denied + Add + Apply + Approval + Attention + Balance: %s + Balance + biometric authentication + biometrics + Buy + Go to %1$s + You have not given access to your camera, please adjust your privacy settings + Cancel + Close + Continue + Copy + Copy address + Create + Delete + Disabled + Dislike + Done + Enable + Enabled + Error + Exchange + Explore + Explore history + Explore transaction history + Explorer + Fee + Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s + Custom + Fast + Market + Slow + Speed and fee + Get addresses + Go to provider + Import + Learn & Earn + Like + Locked + Main network + Network fee + Next + No + No address + No data + OK + Primary Card + Paste + Read more + Receive + Reject + Reload + Rename + Save changes + Search + Search tokens + Seed phrase + Select action + Sell + Send + The server is not available, please try again later + Share + Sign + Sign and send + Stake + Start + Submit + Success + Support + Swap + terms and conditions + Transaction failed + Transactions + Transfer + I understand + There was an error. Please try again. + Unreachable + Yes + Contract address copied! + Available networks + Add token + Contract address + Contract address is invalid + Derivation path is invalid + Please select the network + Required field + Decimal must be a valid integer, up to %li + Custom derivation + E. g. m/00\'/0000\'/0\'/0/0 + Enter custom derivation + Decimals + Derivation Path + Default + BIP44 coin type + The derivation path you\'ve entered is not valid + E.g. USD Coin + Name + Not selected + Network + Token network + You can manually add a token that is not natively supported by Tangem + E.g. USDC + Symbol + Token symbol + This token/network has already been added to your list + Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing. + Be aware of adding scam tokens, they can cost nothing + Note that tokens can be created by anyone + Chat + Access code + You will have to submit the correct access code before scanning the card + Long Tap + This mechanism protects against proximity attacks on a card. It will enforce a delay between reception and execution of a command. + Passcode + Before executing any command entailing a change of the card state, you will have to enter the passcode. + Referral program + Flip your device screen down to quickly hide and show balances + Privacy policy + %s hashes + Card ID + Contact to Support + Link More Cards + App Currency + Flip-to-Hide Balances + Issuer + Signed + Details + Check your internet connection or switch to a different network + Terms of Service + Oops, the current version of the application is not ready to work with this card, please check for updates. + You have used a card from another wallet. Tap the card associated with this wallet + My tokens + You haven\'t added any tokens yet. Add tokens via Market to swap + Cannot be swapped for %s + Provided by + Provided by %s + Additionally, the network fee for sending the exchanged funds back to your address is included in the rate + Status + Tangem offers token swaps via 3rd-party providers according to each provider\'s terms + Choose provider + An error occurred. Code: %s + Estimated amount + Exchange by %s + Visit provider’s website to refund your money + Operation failed by provider + Visit provider’s website for verification + KYC verification required by provider + Canceled + Confirmed + Confirming + Confirming... + Exchanged + Exchanging + Exchanging... + Failed + Deposit received + Awaiting deposit + Awaiting deposit... + Refunded + Refunding... + Sending to you + Sending to you... + Sent + Provider-sourced data. Estimated amount subject to change due to market conditions. + Exchange status + Verified + Verification required + List of all tokens added to your wallet + Fetching best rates... + Floating rate + By using swap functionality, you agree with provider’s %s + By using swap functionality, you agree with provider’s %1$s and %2$s + More providers are coming soon.\nStay tuned! + Privacy Policy + Provider + Best rate + Available up to %s + Available from %s + Unavailable for this pair + Permission Required + Terms of Use + No tokens found. Please try another request + ID: %s + Transaction ID copied + The following information is optional. You can erase it if you don\'t want to share it. + Tell us what functions you are missing, and we will try to help you. + Please tell us what card do you have + Hi support team, + Please tell us more about your issue. Every small detail can help. + My suggestions + Can\'t scan a card + Feedback + Tangem feedback + Can\'t send a transaction + Order card + Scan card + To change the access code tap the card as shown above and do not remove until the end of the operation + To change the passcode tap the card as shown above and do not remove until the end of the operation + To create the wallet tap the card as shown above and do not remove until the end of the operation + To reset to factory settings tap the card as shown above and do not remove until the end of the operation + Tap to scan + To sign tap the card as shown above and do not remove until the end of the operation + Tap to sign + Tap the card + Internal error: wallet manager not found + You have updated biometrics, scan your card to enter + To begin tracking your crypto assets and transactions, add tokens + You have completed all of the lessons, and are now eligible to receive your 1INCH tokens + + Complete three lessons and receive %d 1INCH token to your wallet + Complete three lessons and receive %d 1INCH tokens to your wallet + + Manage tokens + Total balance + The amount does not include some of your funds + 1INCH tokens will be credited to your %s wallet address within 48 hours + To access all the networks you need to scan the card + Scan your card + Enjoy %1$s service fees on swaps via Changelly from February %2$s-%3$s + Swap with Changelly, %s fees + Tokens + Add + Edit + Couldn’t find this token, you can add it manually + Coin market cap + Blockchain the cryptocurrency was initially created + Native network + Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk. + Not original or primary blockchain the token is hosted + Non-native networks + Networks + Choose networks + Wallet + Couldn’t find this token, you can add it manually + + %1$d of %2$d wallet + %1$d of %2$d wallets + + e.g. BTC I trust, hodl I must + Coin market cap + The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it. + Upvote + Choose wallet + The wallet doesn\'t support more than one network + You have to set up a single access code to protect all your wallets + Protect + You can set up an individual access code on each card later + Personalize + The access code can be restored with a linked card, don\'t keep all cards in one place + Restore + Choose any word, phrase, or number you want as your access code + Create Access Code + Re-enter your Access Code + Access code must be at least 4 characters long + Entered access code didn\'t match the initial access code + Please repeat the operation. The card will be reset to factory settings. + Activation error + You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process? + The backup process is partly complete. You can\'t exit it now. + Add a backup card + Scan the card #%d + Backup now + Scan the primary card + Claim + Continue to my wallet + Finalize the backup + Set PIN code + Receive crypto + Scan primary card + Skip for later + How does it work? + Let\'s generate all the keys on your card and create a secure wallet + Create wallet + Create a wallet + Other options + Your keys will be securely generated inside the card. There is no seed phrase, which means nobody can export or steal it. + Generate keys privately + Your card is activated and ready to be used + Success! + In this case, you will need to start from the beginning. + Do you want to exit the activation process? + Getting started + Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet? + Pin code + Connect + Creating a backup + Read more about seed phrase + Write these 12 words down in the order given below and store them in a safe and secret place. + Your seed phrase + To import your wallet, enter your seed phrase in the field below + Generate seed phrase + Import wallet + A seed phrase is a series of words that allows you to recover your wallet. Unlike the keys generated by the card, seed phrases are unprotected and can be copied and stolen. Use this option at your own risk. + Use seed phrase + Invalid seed phrase. Please check the word order. + Invalid seed phrase. Please check your spelling. + Legacy + To check whether you’ve written down your seed phrase correctly, please enter the 2nd, 7th and 11th words + So, let’s check + To get started, simply claim wxDAI to your wallet + It will take a few seconds + To start the backup process add up to two backup cards. + You can add one more card or finalize the backup process + Prepare the backup card with number %s + Scan the primary card to start the backup process. + Prepare the primary card with number %s + Congratulations! Your first payment crypto card has been activated! + Your wallet card is configured and ready for use. + Max number of cards added. Finalize the backup process. + Activating card + Backup card #%d + Claim %s + Claiming + No backup cards + One backup card added + PIN Code + Prepare your card + Two backup cards added + To get started, simply top up the wallet with any amount + To get started, simply top up the wallet with more than %1$s %2$s + Buy crypto + Show the wallet\'s address + Activate a wallet + The twinning process is partly complete. You can\'t exit it now. + If the process of creating the wallet gets interrupted in any way, you\'ll have to start over + You can backup your keys up to two other blank Tangem Wallet cards. + Access code can be restored with one of backup cards. + All the backup cards can be used as full-functional with the identical keys. + You will be able to set an access code to protect your wallets. + Backup wallet + Access code restore + Identical cards + Access code + Group + By balance + Organize tokens + Ungroup + Select from the gallery + Settings + You have not given access to your camera + Camera access denied + Share feedback + Rate the app 🙌 + Share your app experience – every detail counts. + Enjoying our app? + %1$s %2$s address on %3$s network + %1$s (%2$s) on %3$s network + Send only %s to this address. Sending any other currency will result in its irreversible loss. + Participate + Failed to load the information about the referral program. Please try again later. + Failed to load the information about the referral program. Error code: %s. Please try again later. + Upcoming payments + Your friends bought + Less + More + No upcoming payments + + for %d wallet + for %d wallets + + Will get ^^%1$s^^ for each wallet bought by your friend on your %2$s network address %3$s ^^30 days after^^ that + Will get + for each wallet bought by your friend on your %1$s network address%2$s + You + Will get a + when buying a wallet on tangem.com + %s discount + Your friend + Personal code copied! + Your personal code + Buy Tangem Wallet with discount!\n%s + Refer your friends to Tangem + You\'ve accepted + By tapping this button you accept + of the referral program + + %d wallet + %d wallets + + Please hold the card until the operation complete + Reset the Card + I understand that after performing this action, I will no longer have access to the current wallet + I realize that I can\'t use this card to recover my access code on the other cards of the current wallet + Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. + Factory Reset will completely delete the wallet from the selected card and remove it from the app. You will not be able to restore the current wallet. + Do you have a bank card from another country and a residence permit or registration outside the Russian Federation? + Russian bank cards are not currently accepted + Log into the app and check your balance without scanning the card + Access the app + Allow to use biometrics + Biometrics will be requested instead of the access code for interactions with your wallet + Access code + It looks like you have biometric authentication disabled, it is necessary to save wallets + Enable biometric authorization + Would you like to use biometrics? + Note that making a transaction with your funds will still require your card + Scan card failed. Try again + Scan Card + Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet. + Get your card ready! + Already included in the entered address + Subtract + Not enough funds to cover the network commission. Subtract the commission from the amount sent? + Fee is increased + Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? + Reason: %1$s\nCode: %2$s + The transaction is not completed + Amount + Confirm + %1$s at %2$s + Address + Destination Tag + Enter address + Address is the same as wallet address + Invalid Tag. It won\'t be added to the transaction. + Invalid Memo. It won\'t be added to the transaction. + Tag + Memo + Include fee + Low + Normal + Priority + Check your network connection + Network fee info unreachable + From + Gas limit + Gas Limit is auto-calculated; raise it during network congestion + Gas price + Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed + Max + Maximum amount + Max fee + Maximum fee amount + Numbers only for Destination Tag + Sending amount will be reduced by %1$s to cover the selected commission level. The recipient will get %2$s. + Network fee coverage + Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance + Total exceeds balance + The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance. + Fee exceeds balance + The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s. + Existential deposit + The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. + Custom fee is high + The fee for transferring the entire balance is higher. To reduce the commission, you can leave %s. + Fee is increased + The included commission exceeds the transfer amount, leading to a negative value + Invalid amount + The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s. + Target account is not created. Please change the amount to send. + The amount to send must be at least %s + Reduce by %s + Reduce to %s + Kindly be aware that your transaction may experience delays under specific fee settings + Transaction delays are possible + Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. + Transaction limitation + Optional + Please align your QR code with the square to scan it. Ensure you scan %s network address. + Recent + Recipient + Not a valid address + Ensure that you are sending funds to an %s wallet address. Errors may result in the loss of your tokens + Send to + A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds + My wallets + Sending... + Sending %s + Total + %1$s and %2$s will be sent + ≈ %1$s (inc. fee: %2$s) + %s will be sent + Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while + Invalid address + %1$s (%2$s) + Transaction sent + Buy now + I have a promo code… + Tangem Wallet + Other payment methods + Pre-order now + Store your crypto assets secure while keeping private keys contained in your card + Revolutionary Hardware Wallet + Up to 3 physical cards to one wallet + Up to + 3 physical cards + to one wallet + Ultra Secure Backup + A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card + Thousands of Currencies + Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. + The Wallet for Everyone + Take three lessons, get a discount on your Tangem Wallet, and receive 1INCH tokens to your wallet + Learn + Meet Tangem + Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services + Web 3.0 Compatible + The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address. + The amount includes the service provider\'s fee. + Fees + All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval. + Approve + Error: %s + You swap + Give Permission + High price impact! + Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. + Insufficient funds + Sending amount will be reduced to cover the selected fee level + Insufficient funds in your %1$s wallet to cover fees. Top up your %2$s wallet first. + Transaction in progress... + Waiting + Approve + Current transaction + The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. + Give Permission + Specify the approve limit for the selected token + Amount %s + Spender + Your wallet + To continue, grant 1inch smart contracts permission to use your %s + Unlimited + View in Explorer + In progress + Swap + Swap %s for + Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product. + You receive + Other tokens + Choose token + Your tokens + not available + Balances hidden + Balances shown + Undo + + %d token + %d tokens + + Hide + You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. + Hide %s + Hide token + %1$s token in %%image%% %2$s network + The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list. + Unable to hide %s + No rate + Exchange this token for another at %1$s service fees from February %2$s-%3$s. + Swap with Changelly, %s fees + Swap now + contract: %s + You don\'t have any transactions yet + Failed to load transaction history.\nClick on reload button to update the information. + Multiple addresses + Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer. + Operation + from: %s + to: %s + In progress… + You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d + You\'ve scanned wrong twin card. Please try another one + This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. + One wallet. Two cards. + Scan the card #%s + Creating wallet + Scan the #%s twin card + Preparing card + Tangem Twin + This action is irreversible. You will not have access to the old wallet. + Tap the twin card with number %s and do not remove until the end of the operation + Use %s or scan a card to have an access to your wallet + Add new wallet + Are you sure you want to delete this wallet? + %d selected + An error has occurred, please scan your card to log in + This wallet has already been saved, you can add another one + Multi-currency + Wallet name + Rename Wallet + Single-currency + My Wallets + Unlock all + Unlock all with %s + Transaction history + Blockchain is unreachable. Try later + Balance is loading… + Scan the card + Transaction is in progress… + Verified Balance + Actions + Choose wallet option + Do you want to buy or sell crypto? + Requesting to sign a message.\n\n%s + Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s + Trade order for %1$s\nPrice: %2$s\nAmount to receive: %3$s\nAmount to pay: %4$s + Transaction details:\nFrom: %1$s\nTo: %2$s\nAmount: %3$s + The BNB transaction has been successfully signed and sent to the Dapp + Clipboard contain WalletConnect code. Use copied value or scan QR-code + Request to create transaction for %1$s\n%2$s\n\nAmount: %3$s\nFee: %4$s\nTotal: %5$s\nBalance: %6$s + Can\'t send transaction. Not enough funds. + Failed to establish WalletConnect session. Please, try again later. + Not all tokens were added to your list. Please add them first and try again. Missing tokens:\n + Failed to sign message.\nPlease, try again + Failed to establish WalletConnect session: timeout error. Please, try again later. + Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n + Connection with this Dapp cannot be established due to its technical implementation. + We\'ve encountered unknown error. Error message: %s. If the problem persists — feel free to contact our support + Wrong card selected in Tangem App + Failed to create transaction from Dapp data. Code: %s + We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support + The message has been successfully signed and sent to the Dapp + %s network not found. Please, add it first and try again. + No opened WalletConnect sessions + Ooops. No Sessions. + Failed to pairing WalletConnect session: %1$s + Paste from clipboard + Message for %1$s:\n%2$s + Request to start a session for\n%1$s\n\nNETWORK: %2$s\n\nURL: %3$s + The operation couldn\'t be completed.\n\nYou have already established a WalletConnect session with this parameters. + Scan new code + This card can\'t be used to establish WalletConnect session + This network is not supported. Please select another network. + Select network + Dapp didn\'t provide essential data to establish WalletConnect session + Failed to find session for request + WalletConnect Sessions + Connect to dApps + WalletConnect + The transaction has been successfully signed and sent to the Dapp + The transaction has been succesfully signed and sent to the blockchain + Failed to find transaction hash + %s network + Account is not created + This card is not supported + Your Tangem card was made to work with a different application. Please see the name and instructions on your card, and install the correct app + %s Market Price + last 24h + %s network + Address was copied to clipboard + No internet connection + Receiving + from %s + Sending + to %s + Tangem + Use %s or scan a card to unlock access to your wallet + Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost. + Backup error + Could be better + Learn more + Like it + Ok, Got it! + Really cool! + Refresh + You are currently in the Demo mode + Demo mode active + The card you scanned is a developer card. Do not use it to create your wallet. + Not for users! + %1$s network requires an Existential Deposit. If your account drops below %2$s, it will be deactivated, and any remaining funds will be destroyed. + Network requires Existential Deposit + Swap will be available after the %s transaction is complete + You have active transaction + Swap approval is underway and will be completed shortly + Approval in progress + The minimum swapping amount is %1$s. Please ensure that the remaining balance after sending will not be less than %2$s. + You do not have any %s exchangeable coins in your list + No available tokens to swap + To make a transaction you need to deposit some %1$s %2$s + Unable to cover %s fee + Service temporarily unavailable + The amount of tokens to be swapped must not exceed %s + The amount to swap must be at least %s + Please change the amount to swap + This card might be a production sample or counterfeit + Authenticity check failed + Only %s signatures are left on this card. You must withdraw all of your funds. + Low signature count + Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds. + + Use your card to get an address for %d network + Use your card to get an addresses for %d networks + + Some addresses are missing + The network is currently unreachable. Please try again later. + Network is unreachable + Top up your wallet + Your wallet hasn\'t been backed up. Carry out this procedure to protect your assets now. + Missing backup + This card has been previously used for transactions. If received from an untrusted source, consider withdrawing all funds. If it\'s your card, no action is required. + Card has already signed transactions + Device incompatibility detected + Your review keeps us motivated to make Tangem Wallet even better + Enjoying Tangem? + Network rent fee required + %1$s is an asset in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee. + Insufficient %1$s to cover network fee + Sending funds will be available once the pending transaction(s) in network %s is complete + Sending funds will be available once the %s transaction is complete + Transaction pending + Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free. + Some networks currently are unreachable. Please try again later. + Some networks are unreachable + This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes. + For testing purposes only + Discard + You have an interrupted backup. Do you want to resume? + Yes, resume + Discard + If you will discard the backup now, then you will have to reset the cards to factory settings to start over again + Resume backup + This is an irreversible action + Log in with %s + Scan card + Use %s or scan a card to access the app + Welcome back! diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index bf5148d51e..c9736b29ad 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -49,7 +49,7 @@ internal sealed class SendNotification(val config: NotificationConfig) { val amountLimit: String, val onConfirmClick: () -> Unit, ) : Error( - title = resourceReference(R.string.send_notifiaction_transaction_limit_title), + title = resourceReference(R.string.send_notification_transaction_limit_title), subtitle = resourceReference( R.string.send_notification_transaction_limit_text, wrappedList(cryptoCurrency, utxoLimit, amountLimit), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 669c2e24f6..a33cc576e3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -165,7 +165,7 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { object TopUpWithoutReserve : Informational( title = resourceReference(id = R.string.warning_no_account_title), - subtitle = resourceReference(id = R.string.no_account_bnb), + subtitle = resourceReference(id = R.string.no_account_send_to_create), ) class HasPendingTransactions(val coinSymbol: String) : Informational( From 7062b730e3d5517d6ddd4bdb289428df60a28044 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Mar 2024 11:42:12 +0500 Subject: [PATCH 002/109] Updated on 2026-08-14 --- .../send/impl/presentation/viewmodel/SendViewModel.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 621edfae39..d66bc61a03 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -475,6 +475,7 @@ internal class SendViewModel @Inject constructor( val isValidAddress = validateAddress(value) uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress) type?.let { analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress)) } + autoNextFromRecipient(type, isValidAddress) } }.saveIn(addressValidationJobHolder) } @@ -519,6 +520,12 @@ internal class SendViewModel @Inject constructor( ), ) } + + private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) { + val isRecent = type == EnterAddressSource.RecentAddress + val isAddressOnly = uiState.recipientState?.memoTextField == null + if (isRecent && isAddressOnly && isValidAddress) onNextClick() + } // endregion // region fee From bf72de17e1f9fee91d6f9ffa5971d9fa96fc4afc Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Mar 2024 11:03:37 +0500 Subject: [PATCH 003/109] Updated on 2026-08-14 --- .../presentation/state/SendStateFactory.kt | 3 +- .../presentation/state/fee/FeeConverter.kt | 4 +-- .../presentation/state/fee/FeeStateFactory.kt | 8 +++--- .../state/fee/SendFeeCustomFieldConverter.kt | 4 +-- .../state/fee/SendFeeStateConverter.kt | 5 ++-- .../fee/custom/EthereumCustomFeeConverter.kt | 6 ++-- .../presentation/viewmodel/SendViewModel.kt | 28 +++++++++++++++---- 7 files changed, 38 insertions(+), 20 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 619ec36aab..53f1815f01 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -31,6 +31,7 @@ internal class SendStateFactory( private val userWalletProvider: Provider, private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, ) { @@ -62,7 +63,7 @@ internal class SendStateFactory( private val feeStateConverter by lazy(LazyThreadSafetyMode.NONE) { SendFeeStateConverter( appCurrencyProvider = appCurrencyProvider, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt index 7b55e0100d..bd8e832a91 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt @@ -13,14 +13,14 @@ import com.tangem.utils.converter.Converter internal class FeeConverter( private val clickIntents: SendClickIntents, private val appCurrencyProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter { private val ethereumCustomFeeConverter by lazy { EthereumCustomFeeConverter( clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index 4154ae14a3..6a2804103f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -20,7 +20,7 @@ import kotlinx.collections.immutable.persistentListOf internal class FeeStateFactory( private val clickIntents: SendClickIntents, private val currentStateProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, private val isFeeApproximateUseCase: IsFeeApproximateUseCase, ) { @@ -28,7 +28,7 @@ internal class FeeStateFactory( SendFeeCustomFieldConverter( clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) } @@ -36,7 +36,7 @@ internal class FeeStateFactory( FeeConverter( clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) } @@ -140,7 +140,7 @@ internal class FeeStateFactory( } private fun isFeeApproximate(fee: Fee): Boolean { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() return isFeeApproximateUseCase( networkId = cryptoCurrencyStatus.currency.network.id, amountType = fee.amount.type, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt index 24c85ee305..cdd0fa60e6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -14,14 +14,14 @@ import kotlinx.collections.immutable.persistentListOf internal class SendFeeCustomFieldConverter( private val clickIntents: SendClickIntents, private val appCurrencyProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter> { private val ethereumCustomFeeConverter by lazy { EthereumCustomFeeConverter( clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt index 2659080a08..864ae1b501 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -9,16 +9,15 @@ import kotlinx.collections.immutable.persistentListOf internal class SendFeeStateConverter( private val appCurrencyProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter { override fun convert(value: Unit): SendStates.FeeState { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() return SendStates.FeeState( feeSelectorState = FeeSelectorState.Loading, fee = null, notifications = persistentListOf(), - rate = cryptoCurrencyStatus.value.fiatRate, + rate = feeCryptoCurrencyStatusProvider().value.fiatRate, appCurrency = appCurrencyProvider(), isFeeApproximate = false, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt index bf12d6f398..950dd0b135 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -28,7 +28,7 @@ import java.math.RoundingMode internal class EthereumCustomFeeConverter( private val clickIntents: SendClickIntents, private val appCurrencyProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter> { override fun convert(value: Fee.Ethereum): ImmutableList { @@ -152,7 +152,7 @@ internal class EthereumCustomFeeConverter( private fun getFeeFormatted(fee: BigDecimal?): TextReference { val appCurrency = appCurrencyProvider() - val rate = cryptoCurrencyStatusProvider().value.fiatRate + val rate = feeCryptoCurrencyStatusProvider().value.fiatRate val fiatFee = rate?.let { fee?.multiply(it) } return stringReference( BigDecimalFormatter.formatFiatAmount( @@ -164,7 +164,7 @@ internal class EthereumCustomFeeConverter( } private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 621edfae39..99ded2ea4b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -1,3 +1,4 @@ + package com.tangem.features.send.impl.presentation.viewmodel import androidx.compose.runtime.getValue @@ -66,6 +67,7 @@ internal class SendViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, + private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val getFeeUseCase: GetFeeUseCase, private val sendTransactionUseCase: SendTransactionUseCase, @@ -108,6 +110,7 @@ internal class SendViewModel @Inject constructor( userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, validateWalletMemoUseCase = validateWalletMemoUseCase, getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase, ) @@ -120,7 +123,7 @@ internal class SendViewModel @Inject constructor( private val feeStateFactory = FeeStateFactory( clickIntents = this, currentStateProvider = Provider { uiState }, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), isFeeApproximateUseCase = isFeeApproximateUseCase, ) @@ -165,6 +168,7 @@ internal class SendViewModel @Inject constructor( private var isAmountSubtractAvailable: Boolean = false private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + private var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var balanceJobHolder = JobHolder() private var balanceHidingJobHolder = JobHolder() @@ -237,6 +241,7 @@ internal class SendViewModel @Inject constructor( onDataLoaded( currencyStatus = it, coinCurrencyStatus = it, + feeCurrencyStatus = getFeeCurrencyStatusSync(it), ) } } @@ -247,11 +252,13 @@ internal class SendViewModel @Inject constructor( combine( flow = getCoinCurrencyStatusUpdates(isSingleWallet = isSingleWallet), flow2 = getCurrencyStatusUpdates(isSingleWallet = isSingleWallet), - ) { coinStatus, currencyStatus -> - if (coinStatus.isRight() && currencyStatus.isRight()) { + ) { coinStatus, maybeCurrencyStatus -> + if (coinStatus.isRight() && maybeCurrencyStatus.isRight()) { + val currencyStatus = maybeCurrencyStatus.getOrElse { error("Currency status is unreachable") } onDataLoaded( - currencyStatus = currencyStatus.getOrElse { error("Currency status is unreachable") }, + currencyStatus = currencyStatus, coinCurrencyStatus = coinStatus.getOrElse { error("Coin status is unreachable") }, + feeCurrencyStatus = getFeeCurrencyStatusSync(currencyStatus), ) } } @@ -274,6 +281,12 @@ internal class SendViewModel @Inject constructor( isSingleWalletWithTokens = isSingleWallet, ).conflate().distinctUntilChanged() + private suspend fun getFeeCurrencyStatusSync(cryptoCurrencyStatus: CryptoCurrencyStatus) = + getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull() ?: error("Fee currency is unreachable") + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> @@ -286,9 +299,14 @@ internal class SendViewModel @Inject constructor( ) } - private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, coinCurrencyStatus: CryptoCurrencyStatus) { + private fun onDataLoaded( + currencyStatus: CryptoCurrencyStatus, + coinCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus, + ) { cryptoCurrencyStatus = currencyStatus coinCryptoCurrencyStatus = coinCurrencyStatus + feeCryptoCurrencyStatus = feeCurrencyStatus if (transactionId != null && amount != null && destinationAddress != null) { uiState = stateFactory.getReadyState(amount, destinationAddress) From 37e16f38ea48968b78f128d5a061711871026330 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Mar 2024 19:21:02 +0800 Subject: [PATCH 004/109] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/TapApplication.kt | 16 +++++++++++++++- .../tap/di/domain/WalletManagersFacadeModule.kt | 6 ++++++ .../com/tangem/tap/domain/TapWalletManager.kt | 5 +++++ .../tangem/tap/proxy/redux/DaggerGraphState.kt | 4 ++++ .../walletmanager/DefaultWalletManagersFacade.kt | 11 ++++++++--- .../walletmanager/utils/WalletManagerFactory.kt | 9 ++++++++- 6 files changed, 46 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 051cb803b8..de7e20d595 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -10,8 +10,10 @@ import com.orhanobut.logger.AndroidLogAdapter import com.orhanobut.logger.Logger import com.tangem.Log import com.tangem.LogFormat +import com.tangem.TangemSdkLogger import com.tangem.blockchain.common.AccountCreator import com.tangem.blockchain.common.datastorage.BlockchainDataStorage +import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.filter.OneTimeEventFilter @@ -178,6 +180,12 @@ internal class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles + + @Inject + lateinit var blockchainSDKLogger: BlockchainSDKLogger + + @Inject + lateinit var tangemSdkLogger: TangemSdkLogger // endregion Injected override fun onCreate() { @@ -266,6 +274,8 @@ internal class TapApplication : Application(), ImageLoaderFactory { saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, cardRepository = cardRepository, feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, + tangemSdkLogger = tangemSdkLogger, + blockchainSDKLogger = blockchainSDKLogger, ), ), ) @@ -343,13 +353,17 @@ internal class TapApplication : Application(), ImageLoaderFactory { Log.Level.View, Log.Level.Network, Log.Level.Error, + Log.Level.Biometric, ) return TangemLogCollector(logLevels, LogFormat.StairsFormatter()) } val additionalFeedbackInfo = initAdditionalFeedbackInfo(context) val tangemLogCollector = initTangemLogCollector() - Log.addLogger(tangemLogCollector) + + Log.addLogger( + logger = if (feedbackManagerFeatureToggles.isLocalLogsEnabled) tangemSdkLogger else tangemLogCollector, + ) val feedbackManager = FeedbackManager( infoHolder = additionalFeedbackInfo, diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt index 3fedeef4ca..319ddd2c0e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt @@ -3,11 +3,13 @@ package com.tangem.tap.di.domain import com.squareup.moshi.Moshi import com.tangem.blockchain.common.AccountCreator import com.tangem.blockchain.common.datastorage.BlockchainDataStorage +import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.walletmanager.DefaultWalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.onboarding.data.MnemonicRepository @@ -32,6 +34,8 @@ internal object WalletManagersFacadeModule { mnemonicRepository: MnemonicRepository, assetReader: AssetReader, @SdkMoshi moshi: Moshi, + blockchainSDKLogger: BlockchainSDKLogger, + feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, ): WalletManagersFacade { return DefaultWalletManagersFacade( walletManagersStore = walletManagersStore, @@ -42,6 +46,8 @@ internal object WalletManagersFacadeModule { moshi = moshi, mnemonic = mnemonicRepository.generateDefaultMnemonic(), accountCreator = accountCreator, + blockchainSDKLogger = blockchainSDKLogger, + feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 03f3022a51..f8a93381e8 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -47,6 +47,11 @@ class TapWalletManager( config = blockchainSdkConfig, accountCreator = store.inject(DaggerGraphState::accountCreator), blockchainDataStorage = store.inject(DaggerGraphState::blockchainDataStorage), + loggers = if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) { + listOf(store.inject(DaggerGraphState::blockchainSDKLogger)) + } else { + emptyList() + }, ) } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 0afb10ab8d..c23def20bc 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -1,7 +1,9 @@ package com.tangem.tap.proxy.redux +import com.tangem.TangemSdkLogger import com.tangem.blockchain.common.AccountCreator import com.tangem.blockchain.common.datastorage.BlockchainDataStorage +import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -67,4 +69,6 @@ data class DaggerGraphState( val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null, val cardRepository: CardRepository? = null, val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles? = null, + val tangemSdkLogger: TangemSdkLogger? = null, + val blockchainSDKLogger: BlockchainSDKLogger? = null, ) : StateType \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index bb0cc5a3ad..15b93ba08e 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -11,6 +11,7 @@ import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchain.common.address.EstimationFeeAddressFactory import com.tangem.blockchain.common.datastorage.BlockchainDataStorage +import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchain.common.pagination.Page import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee @@ -25,6 +26,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -53,15 +55,18 @@ class DefaultWalletManagersFacade( configManager: ConfigManager, blockchainDataStorage: BlockchainDataStorage, accountCreator: AccountCreator, + blockchainSDKLogger: BlockchainSDKLogger, + feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, ) : WalletManagersFacade { private val demoConfig by lazy { DemoConfig() } private val resultFactory by lazy { UpdateWalletManagerResultFactory() } private val walletManagerFactory by lazy { WalletManagerFactory( - configManager, - accountCreator, - blockchainDataStorage, + configManager = configManager, + accountCreator = accountCreator, + blockchainDataStorage = blockchainDataStorage, + blockchainSDKLogger = if (feedbackManagerFeatureToggles.isLocalLogsEnabled) blockchainSDKLogger else null, ) } private val sdkTokenConverter by lazy { SdkTokenConverter() } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt index e0377cf6a3..89a63b05f6 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationParams import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.datastorage.BlockchainDataStorage +import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.config.ConfigManager import com.tangem.domain.common.DerivationStyleProvider @@ -18,10 +19,16 @@ internal class WalletManagerFactory( configManager: ConfigManager, accountCreator: AccountCreator, blockchainDataStorage: BlockchainDataStorage, + blockchainSDKLogger: BlockchainSDKLogger? = null, ) { private val sdkWalletManagerFactory by lazy { - BlockchainWalletManagerFactory(configManager.config.blockchainSdkConfig, accountCreator, blockchainDataStorage) + BlockchainWalletManagerFactory( + config = configManager.config.blockchainSdkConfig, + accountCreator = accountCreator, + blockchainDataStorage = blockchainDataStorage, + loggers = listOfNotNull(blockchainSDKLogger), + ) } fun createWalletManager( From 498718843aeef89e140b4305fdda052d72f04be8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Mar 2024 11:30:29 +0300 Subject: [PATCH 005/109] Updated on 2026-08-14 --- app/build.gradle.kts | 8 ++++ .../tangem/helpers/base/BaseAutoTestCase.kt | 27 +++++++++++ .../com/tangem/screens/StoriesScreen.kt | 32 +++++++++++++ .../kotlin/com/tangem/tests/StoriesTest.kt | 45 +++++++++++++++++++ .../tangem/tap/common/compose/resources/C.kt | 9 ++++ .../features/home/compose/StoriesScreen.kt | 4 +- .../home/compose/views/HomeButtons.kt | 10 ++++- gradle/dependencies.toml | 14 +++++- 8 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/helpers/base/BaseAutoTestCase.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt create mode 100644 app/src/main/java/com/tangem/tap/common/compose/resources/C.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 553088615c..ee0bc5020b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,6 +11,9 @@ plugins { android { namespace = "com.tangem.wallet" + testOptions { + animationsDisabled = true + } } configurations.all { @@ -207,6 +210,11 @@ dependencies { testImplementation(deps.test.truth) androidTestImplementation(deps.test.junit.android) androidTestImplementation(deps.test.espresso) + androidTestImplementation(deps.test.espresso.intents) + androidTestImplementation(deps.test.kaspresso) + androidTestImplementation(deps.test.kaspresso.compose) + androidTestImplementation(deps.test.compose.junit) + androidTestImplementation(deps.test.hamcrest) /** Chucker */ debugImplementation(deps.chucker) diff --git a/app/src/androidTest/kotlin/com/tangem/helpers/base/BaseAutoTestCase.kt b/app/src/androidTest/kotlin/com/tangem/helpers/base/BaseAutoTestCase.kt new file mode 100644 index 0000000000..6e0bfc19b2 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/helpers/base/BaseAutoTestCase.kt @@ -0,0 +1,27 @@ +package com.tangem.helpers.base + +import android.Manifest +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule +import com.kaspersky.components.composesupport.config.withComposeSupport +import com.kaspersky.kaspresso.kaspresso.Kaspresso +import com.kaspersky.kaspresso.testcases.api.testcase.TestCase +import com.tangem.tap.MainActivity +import org.junit.Rule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +open class BaseAutoTestCase : TestCase( + kaspressoBuilder = Kaspresso.Builder.withComposeSupport() +) { + + @get:Rule + open val composeTestRule = createAndroidComposeRule() + + @get: Rule + val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( + Manifest.permission.POST_NOTIFICATIONS, + Manifest.permission.CAMERA + ) +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt new file mode 100644 index 0000000000..38bdbce846 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt @@ -0,0 +1,32 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.tap.common.compose.resources.C +import com.tangem.wallet.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.views.KView +import io.github.kakaocup.kakao.text.KButton + +class StoriesScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(C.Tag.STORIES_SCREEN) } + ) { + + val scanButton: KNode = child { + hasTestTag(C.Tag.STORIES_SCREEN_SCAN_BUTTON) + } + + val orderButton: KNode = child { + hasTestTag(C.Tag.STORIES_SCREEN_ORDER_BUTTON) + } + + val enableNFCAlert: KView = KView { + withId(R.id.alertTitle) + } + + val cancelButton: KButton = KButton { + withId(android.R.id.button2) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt new file mode 100644 index 0000000000..ce58b4a1be --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt @@ -0,0 +1,45 @@ +package com.tangem.tests + +import android.content.Intent.ACTION_VIEW +import androidx.test.espresso.intent.Intents +import com.tangem.helpers.base.BaseAutoTestCase +import com.tangem.screens.StoriesScreen +import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.kakao.intent.KIntent +import org.junit.Test + +class StoriesTest : BaseAutoTestCase() { + + @Test + fun clickOnButtons() = before { + Intents.init() + }.after { + Intents.release() + }.run { + ComposeScreen.onComposeScreen(composeTestRule) { + step("Click on \"Scan\" button") { + scanButton { + assertIsDisplayed() + performClick() + } + } + step("Assert: \"Scan card\" popup opened") { + enableNFCAlert.isDisplayed() + cancelButton.click() + device.uiDevice.pressBack() + } + step("Click on \"Order\" button") { + orderButton.performClick() + } + step("Assert: browser opened") { + val expectedIntent = KIntent { + hasAction(ACTION_VIEW) + hasData(NEW_BUY_WALLET_URL) + } + expectedIntent.intended() + device.uiDevice.pressBack() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/resources/C.kt b/app/src/main/java/com/tangem/tap/common/compose/resources/C.kt new file mode 100644 index 0000000000..ca77ba62d2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/resources/C.kt @@ -0,0 +1,9 @@ +package com.tangem.tap.common.compose.resources + +object C { + object Tag { + const val STORIES_SCREEN = "STORIES_SCREEN_CONTAINER" + const val STORIES_SCREEN_SCAN_BUTTON = "STORIES_SCREEN_SCAN_BUTTON" + const val STORIES_SCREEN_ORDER_BUTTON = "STORIES_SCREEN_ORDER_BUTTON" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt index 73a454fe10..bd3f2a5dde 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt @@ -15,11 +15,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.common.compose.resources.C import com.tangem.tap.features.home.compose.content.* import com.tangem.tap.features.home.compose.views.HomeButtons import com.tangem.tap.features.home.compose.views.SearchCurrenciesButton @@ -55,7 +57,7 @@ fun StoriesScreen( } StoriesScreenContent( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().testTag(C.Tag.STORIES_SCREEN), config = StoriesScreenContentConfig( storiesSize = state.stories.lastIndex, currentStoryIndex = currentStoryIndex, diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt index 0533961886..75d308e2b7 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -15,6 +16,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.common.compose.resources.C import com.tangem.wallet.R @Composable @@ -29,13 +31,17 @@ internal fun HomeButtons( modifier = modifier, ) { ScanCardButton( - modifier = Modifier.weight(weight = 1f), + modifier = Modifier + .weight(weight = 1f) + .testTag(C.Tag.STORIES_SCREEN_SCAN_BUTTON), showProgress = btnScanStateInProgress, onClick = onScanButtonClick, ) SpacerW12() OrderCardButton( - modifier = Modifier.weight(weight = 1f), + modifier = Modifier + .weight(weight = 1f) + .testTag(C.Tag.STORIES_SCREEN_ORDER_BUTTON), onClick = onShopButtonClick, ) } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 60e7423e4c..4b9302bf6b 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -97,11 +97,16 @@ detekt = "1.22.0" # endregion Tools # region Testing -espresso = "3.4.0" +espresso = "3.5.1" +espresso-intents = "3.5.1" junit = "4.13.2" -junitAndroidExt = "1.1.3" +junitAndroidExt = "1.1.5" mockk = "1.13.4" truth = "1.1.3" +kaspresso = "1.5.4" +kaspresso-compose = "1.5.4" +compose-junit = "1.6.2" +hamcrest = "2.2" # endregion Testing [plugins] @@ -183,10 +188,15 @@ detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", # region Test test-coroutine = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutine" } test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } +test-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espresso-intents" } test-junit = { module = "junit:junit", version.ref = "junit" } test-junit-android = { module = "androidx.test.ext:junit", version.ref = "junitAndroidExt" } test-truth = { module = "com.google.truth:truth", version.ref = "truth" } test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +test-kaspresso = { module = "com.kaspersky.android-components:kaspresso", version.ref = "kaspresso" } +test-kaspresso-compose = { module = "com.kaspersky.android-components:kaspresso-compose-support", version.ref = "kaspresso-compose"} +test-compose-junit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-junit" } +test-hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } # endregion Test # region Other From 17763224957252d0e83984db9cda3c101d7cac4c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Mar 2024 11:31:53 +0100 Subject: [PATCH 006/109] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 12 --- core/res/src/main/res/values-fr/strings.xml | 12 --- core/res/src/main/res/values-it/strings.xml | 12 --- core/res/src/main/res/values-ru/strings.xml | 77 ------------------- .../src/main/res/values-zh-rTW/strings.xml | 60 --------------- core/res/src/main/res/values/strings.xml | 75 +----------------- 6 files changed, 1 insertion(+), 247 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index aee0c9f45e..15fe23fdca 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,7 +1,6 @@ Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet - Tangem Bot Akzeptieren Bilanz: %s Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an @@ -34,7 +33,6 @@ Legen Sie die Karte zum Scannen an Tippen um zu signieren Legen Sie die Karte an - Der Betrag enthält nicht einige Ihrer Mittel Ein wallet erstellen Betrag Adresse @@ -54,19 +52,9 @@ Ungültige Adresse Tangem Wallet Tangem Twin - Die Bilanz wird aufgeladen… - Die Transaktion läuft… - Verifizierte Bilanz WalletConnect - Das Konto ist nicht erstellt - Diese Karte wird nicht unterstützt - Ihre Tangem-Karte wurde für die Arbeit mit einer anderen App ausgestellt. Bitte sehen Sie sich den Namen und die Anleitung auf Ihrer Karte an und installieren Sie eine korrekte App Die Adresse wurde erfolgreich kopiert Keine Internetverbindung - Erhalten - von %s - Senden - an %s Tangem OK, ich hab\'s! diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index bceed42d51..0901cc1225 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1,7 +1,6 @@ Cette carte n\'est pas conçue pour fonctionner avec Tangem - Tangem Bot J\'accepte Solde : %s Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité @@ -34,7 +33,6 @@ Posez pour scanner Touchez pour signer Posez la carte - Le montant n\'inclut pas certains de vos fonds Créer un portefeuille Somme Adresse @@ -54,19 +52,9 @@ Adresse incorrecte Tangem Wallet Tangem Twin - Solde est en cours de téléchargement… - Transaction en cours… - Solde confirmé WalletConnect - Compte n\'est pas créé - Cette carte n\'est pas prise en charge - Votre carte Tangem a été créée pour fonctionner avec une autre application. Regardez le nom et les instructions sur votre carte et installez l\'application pertinente L\'adresse a été copiée avec succès Pas de connexion internet - Réception - de %s - Envoi - jusqu\'à %s Tangem Ok, je l\'ai! diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 353953eb8e..2c944c1c4a 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -1,7 +1,6 @@ Questa carta non è progettata per funzionare con Tangem - Tangem Bot Accetta Saldo: %s Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy @@ -34,7 +33,6 @@ Avvicina per scansionare Avvicina per firmare Avvicina la carta - L\'importo non include alcuni dei tuoi fondi Crea portafoglio Importo Indirizzo @@ -54,19 +52,9 @@ Indirizzo non valido Tangem Wallet Tangem Twin - Il saldo sta per essere caricato… - Transazione in corso… - Saldo verificato WalletConnect - Conto non creato - Questa carta non è supportata - La tua carta Tangem è stata creata per funzionare con un\'altra applicazione. Leggere il nome e le istruzioni sulla carta e installare l\'applicazione corretta L\'indirizzo è stato copiato con successo Nessuna connessione a Internet - Riceviu - da %s - Inviau - su %s Tangem Ok, ho capito! diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index d0e06ddf85..8474f7f533 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -6,7 +6,6 @@ Спасибо за ваш отзыв Отправлено успешно Обратиться в поддержку - Отправить отзыв Эта функция недоступна в демонстрационном режиме Не удалось отправить письмо Причина: %s @@ -54,14 +53,10 @@ Код доступа будет изменен только на данной карте Заводские настройки Тип безопасности - Выбранный способ защиты приложения Настройки карты - Tangem Bot Чат - Отправить логи Принять Доступ запрещен - Добавить Применить Одобрение Внимание @@ -80,12 +75,10 @@ Создать Удалить Отключено - Не нравится Готово Включить Включено Ошибка - Обменять Обозреватель Посмотреть историю Посмотреть историю транзакций @@ -100,14 +93,12 @@ Получить адреса К провайдеру Импортировать - Нравится Заблокирован Основная сеть Сетевая комиссия Далее Нет Нет адреса - Нет данных OK Основная карта Вставить @@ -148,7 +139,6 @@ Адрес контракта некорректен Путь деривации некорректен Пожалуйста, выберите сеть - Обязательное поле Своя деривация Например m/00\'/0000\'/0\'/0/0 Введите свою деривацию @@ -179,7 +169,6 @@ Перед выполнением любой команды, влекущей за собой изменение состояния карты, вам необходимо будет ввести пароль. Реферальная программа Переверните экран вашего устройства вниз, чтобы быстро скрыть и отобразить балансы - Privacy policy %s хэшей Номер карты Обратиться в поддержку @@ -191,14 +180,12 @@ Подробности Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования - К сожалению, текущая версия приложения не готова к работе с этой картой, проверьте наличие обновлений Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком. Мои токены У вас нет добавленных токенов. Добавьте токены для обмена Недоступен для обмена с %s Предоставлено Предоставлено %s - Кроме того, в курс обмена включена комиссия сети за отправку обмененных средств на ваш адрес Статус Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами Выберите провайдера @@ -265,20 +252,9 @@ Чтобы подписать транзакцию, приложите карту как показано выше и не убирайте до окончания операции Приложите, чтобы подписать Приложите карту - Внутренняя ошибка: не удается найти менеджер кошельков Вы обновили данные биометрии, отсканируйте свою карту для входа Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены - Вы успешно прошли все уроки и теперь можете получить 1INCH токены - - Пройдите 3 урока и получите %d 1INCH токен на свой кошелек - Пройдите 3 урока и получите %d 1INCH токена на свой кошелек - Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек - Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек - Управление токенами - Баланс - В сумме учтены не все монеты - 1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту Отсканируйте карту Обменивайте свои токены с %1$s комиссии провайдера через Changelly с %2$s по %3$s февраля. @@ -289,7 +265,6 @@ Основная сеть Не основной или основной блокчейн, на котором размещен токен Не основные сети - Сети Выберите сети Кошелек Не удалось найти этот токен, вы можете добавить его вручную. @@ -324,10 +299,8 @@ Сканировать карту #%d Создать резервную копию Сканировать основную карту - Запросить Перейти к моему кошельку Завершение бэкапа - Установить Код доступа Получить криптовалюту Сканировать основную карту Пропустить @@ -344,8 +317,6 @@ Вы хотите выйти из процесса активации? Подготовка Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа? - Код доступа - Подключиться Резервная копия Прочитать о seed-фразе Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте. @@ -360,23 +331,17 @@ Устаревший Чтобы проверить, правильно ли вы записали seed-фразу, введите 2-е, 7-е и 11-е слова Итак, проверим - Для начала работы просто запросите начисление wxDAI на свой кошелек - Это займет несколько секунд Чтобы начать процесс резервного копирования, добавьте одну или две резервные карты. Вы можете добавить еще одну карту или завершить процесс резервного копирования Подготовьте резервную карту с номером %s Отсканируйте основную карту, чтобы начать процесс резервного копирования. Подготовьте основную карту с номером %s - Поздравляем! Ваша платежная крипто карта теперь активирована! Ваша карта настроена и готова к использованию. Добавлено максимальное количество карт. Завершите процесс резервного копирования. Активация карты Резервная карта #%d - Запросить %s - Запрашивается Нет резервных карт Добавлена ​​одна резервная карта - Код доступа Подготовьте свою карту Добавлены две резервные карты Пополните кошелек на любую сумму, чтобы начать пользоваться картой @@ -420,8 +385,6 @@ за %d кошельков Получите ^^%1$s^^ на ваш адрес в сети %2$s %3$s ^^спустя 30 дней^^ за каждый кошелек, который купит ваш друг - Получите - на ваш адрес в сети %1$s%2$s за каждый кошелек, который купит ваш друг Вы Получит при покупке кошелька на сайте tangem.com @@ -440,7 +403,6 @@ %d кошельков %d кошельков - Пожалуйста, удерживайте карту до завершения операции Сбросить карту Я понимаю, что после выполнения этого действия у меня больше не будет доступа к текущему кошельку Я понимаю, что не смогу этой картой восстановить пароль на остальных картах этого кошелька, если я его забуду @@ -457,7 +419,6 @@ Включите биометрическую аутентификацию Вы хотите использовать биометрию? Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта - Ошибка сканирования карты. Попробуй снова. Сканировать Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку. Приготовьте свою карту @@ -494,8 +455,6 @@ Покрытие сетевой комиссии Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств - Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети. - Комиссия превышает баланс Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Установлена высокая комиссия Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s. @@ -529,7 +488,6 @@ У меня есть промо-код… Tangem Wallet Другие способы оплаты - Сделать предзаказ Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Революционный аппаратный кошелек До трех карт с одним кошельком @@ -554,11 +512,9 @@ Ошибка: %s Вы отправляете Дать разрешение - Сильные колебания цены! Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. Недостаточно средств Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии - Недостаточно средств для оплаты комиссии на вашем %1$s кошельке для создания транзакции. Сначала пополните свой %2$s кошелек. Транзакция в процессе… Подождите Подтвердить @@ -567,29 +523,16 @@ Дать разрешение Укажите лимит доступа к выбранному токену Количество %s - Отправитель - Ваш кошелек Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s Безлимитно - Открыть в обозревателе В процессе Обменять - Обмен %s на - Котировки включают дополнительную комиссию Tangem в размере %s. Это помогает нам предоставлять первоклассный продукт. Вы получите - Другие токены Выберите токен - Ваши токены не доступен Балансы скрыты Балансы показаны Отменить - - %d токен - %d токена - %d токенов - %d токенов - Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. Скрыть %s @@ -597,7 +540,6 @@ %1$s токен в сети %%image%% %2$s Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. Невозможно скрыть %s - Нет цены Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля. Обмен с Changelly, %s комиссии Обменять @@ -609,7 +551,6 @@ Операция от: %s на: %s - В процессе… Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую Это карта, которую вы держите в руках. У парной карты номер %s.\n\nОбе карты можно использовать для вывода средств из этого кошелька. @@ -624,25 +565,15 @@ Используйте %s или отсканируйте карту, чтобы получить доступ к своему кошельку Добавить новый кошелек Вы уверены, что хотите удалить этот кошелек? - %d выбрано Произошла ошибка, пожалуйста, отсканируйте свою карту для входа Этот кошелек уже был сохранен, вы можете добавить другой - Мультивалютные Имя кошелька Переименование кошелька - Одновалютные - Мои кошельки Разблокировать все Разблокировать все с %s История транзакций Блокчейн недоступен. Попробуйте позже. - Баланс загружается… Отсканируйте карту - Транзакция подтверждается… - Подтвержденный баланс - Действия - Что отправить? - Вы хотите купить или продать криптовалюту? Запрос на подпись сообщения.\n\n%s Dapp %1$s, запрос на\nподпись транзакции с BNB.\n\n%2$s Торговый ордер на %1$s\nЦена: %2$s\nСумма к получению: %3$s\nСумма к оплате: %4$s @@ -682,19 +613,11 @@ Транзакция успешно подписана и отправлена ​​в Dapp Транзакция успешно подписана и отправлена ​​в блокчейн Не удалось найти хэш транзакции - Сеть %s - Аккаунт не создан - Эта карта не поддерживается - Ваша карта Tangem предназначена для работы с другим приложением. Пожалуйста, ознакомьтесь с названием и инструкциями на вашей карте и установите правильное приложение. Рыночная цена %s за 24 часа Сеть %s Адрес скопирован в буфер обмена Нет соединения с интернетом - Получение - от %s - Отправка - к %s Tangem Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян. diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index b3e418f344..b5cdf09a4e 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -6,7 +6,6 @@ 謝謝您的反饋 成功送出 請求支持 - 發送反饋 此功能不在展示模式中提供 發送電子郵件失敗 原因:%s @@ -39,13 +38,9 @@ 訪問密碼將僅在此卡上更改 回復至原廠設置 安全模式 - 選定的應用程序保護方法 卡片設置 - Tangem 機器人 支援 - 發送logs 接受 - 添加 注意 餘額: %s 餘額 @@ -61,15 +56,12 @@ 創造 刪除 禁用 - 不喜歡 完成 允許 啟用 錯誤 - 交易 費用 導入 - 喜歡 網路費 OK @@ -102,7 +94,6 @@ 無效地址 衍生路徑錯誤 請選擇網路 - 必填 小數點 默認 BIP44 幣種 @@ -122,7 +113,6 @@ 密碼 在執行任何需要更改卡狀態的命令之前,您必須輸入密碼 推薦計畫 - 隱私政策 %s hashes 卡號 連結更多錢包 @@ -132,7 +122,6 @@ 更多 檢查您的網路連接或切換到其他網絡 服務條款 - 糟糕,當前版本的應用程序無法使用此卡,請檢查更新 您使用了另一個錢包中的卡。點按與此錢包關聯的卡片 以下信息是可選的。如果不想共享,可以將其刪除 告訴我們您缺少哪些功能,我們會盡力幫助您 @@ -153,11 +142,8 @@ 要簽名,請完全按照上圖所示連接手機和卡 點擊簽名 點按卡片 - 內部錯誤:找不到錢包管理器 您已更新生物識別登入,掃描您的卡進入 管理代幣 - 總餘額 - 該金額不包括您的部分資金 要訪問所有的網路您需要掃描卡片 掃描卡片 代幣 @@ -178,10 +164,8 @@ 掃描卡片 #%d 立即備份 掃描主卡 - 獲取 繼續至我的錢包 完成備份過程 - 設置PIN碼 接收貨幣 掃描主卡 暫時略過 @@ -198,8 +182,6 @@ 您想要離開啟用程序嗎? 開始 您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎? - PIN 碼 - 連接 創建備份 閱讀更多關於助記詞的訊息 按照下面給出的順序寫下這 12 個單詞,並將它們存放在安全秘密的地方。 @@ -208,23 +190,17 @@ 使用助記詞 Legacy 那麼,讓我們檢查一下 - 要開始,只需將 wxDAI 獲取到您的錢包 - 這會花上幾秒 要開始備份過程,最多可添加兩張備份卡。 您可以再添加一張卡或完成備份過程 準備編號為 %s 的備份卡 掃描主卡以啟動備份過程 準備編號為 %s 的主卡 - 恭喜! 您的第一張支付加密卡已啟用! 您的錢包卡已配置完畢,可以使用了 已添加至最大數量。完成備份過程 啟用卡片 備份卡 #%d - 獲取%s - 獲取中 沒有備用卡 添加了一張備用卡 - PIN 碼 準備好你的卡片 添加了兩張備用卡 要開始使用,只需為錢包充值任意金額 @@ -246,8 +222,6 @@ 無法加載有關推薦計劃的消息。請稍後再試 無法加載有關推薦計劃的消息。原因:%s。請稍後再試 您的朋友買 - 會得到 - 對於你的朋友在你的 %1$s 網絡地址%2$s上購買的每個錢包 得到 %s 折扣 @@ -262,7 +236,6 @@ %d 錢包 - 請持有卡片直至操作完成 重置卡片 我了解執行此操作後,我將無法再訪問當前錢包 恢復原廠設置將從所選卡中完全刪除錢包。您將無法恢復當前錢包或使用卡恢復訪問密碼 @@ -277,7 +250,6 @@ 啟用生物識別授權 您想使用生物識別技術嗎? 請注意,用您的資金進行交易仍需要您的卡片 - 掃卡失敗。再試一次 掃描卡片 掃描卡片以更改其設置。這些更改只會影響您掃描過的卡,不會影響綁定到您錢包的其他卡。 準備好您的卡! @@ -322,40 +294,26 @@ 批准 錯誤: %s 賦予權限 - 價格影響高! 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量 餘額不足 - 您的 %1$s 錢包中沒有足夠的資金來創建交易。首先為您的 %2$s 錢包充值 交易進行中... 等待中 允許 賦予權限 數量 %s - 花費者 - 您的錢包 要繼續,您需要允許 1inch 智能合約使用您的 %s - 在瀏覽器中查看 進行中 交易 - 交易 %s 至 - 此外,報價包括%s的 Tangem 費用。這有助於我們提供一流的產品 - 其他代幣 選擇代幣 - 您的代幣 無法使用 - - %d 代幣 - 隱藏 您即將在主屏幕上隱藏此代幣。您可以隨時通過管理代幣頁面將其添加回來。 隱藏 %s 隱藏代幣 %1$s 代幣是 %2$s 網絡上的主要貨幣,只要列表中還有該網絡上的其他代幣,它就無法被隱藏。 無法隱藏 %s - 無費用 您還沒有任何交易 無法加載交易 - 進行中… 您掃描了同一張卡片。要創建雙錢包,您需要掃描編號為 %d 的卡 你掃描錯了胞胎卡。請嘗試另一個 這一個是你手裡拿著的,另一個是編號為 %s 的,這兩張卡都可以用來從這個錢包中提取資金 @@ -369,24 +327,14 @@ 將您的 iPhone 靠近編號為 %s 的雙胞胎卡 添加新錢包 您確定要刪除此錢包? - 已選擇 %d 發生錯誤,請掃卡登錄 此錢包已保存,您可以再添加一個 - 多幣種 錢包名稱 重新命名錢包 - 單一幣種 - 我的錢包 用 %s 解鎖全部 交易記錄 區塊鍊無法使用。稍後再試 - 餘額加載中 掃描卡片 - 交易進行中 - 檢視餘額 - 動作 - 選擇錢包選項 - 您想要購買或賣出交易貨幣? 請求籤署消息。%s Dapp %1$s,請求\n簽署 BNB 交易。\n%2$s %1$s 的交易訂單\n價格: %2$s\n接收金額:%3$s\n支付數量: %4$s @@ -425,17 +373,9 @@ 交易已成功簽署並發送至 Dapp 交易已成功簽署並發送至區塊鏈 未能找到交易哈希 - %s 網路 - 帳號尚未被創造 - 不支持此卡 - 您的 Tangem 卡是為與不同的應用程序一起工作而設計的。請查看卡片上的名稱和說明,並安裝正確的應用程序 %s市場價格 地址已復製到剪貼板 無網路 - 接收中 - 來自%s - 發送中 - 至%s Tangem 可以更好 了解更多 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index dc081f161b..d07bb17a98 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -6,7 +6,6 @@ Thank you for your feedback Sent successfully Request support - Send feedback This feature is disabled in Demo mode Failed to send the email Reason: %s @@ -52,14 +51,10 @@ Access code will be changed on this card only Reset to Factory Settings Security Mode - Selected application protection method Card Settings - Tangem Bot Support - Send logs Accept Access denied - Add Apply Approval Attention @@ -78,12 +73,10 @@ Create Delete Disabled - Dislike Done Enable Enabled Error - Exchange Explore Explore history Explore transaction history @@ -99,14 +92,12 @@ Go to provider Import Learn & Earn - Like Locked Main network Network fee Next No No address - No data OK Primary Card Paste @@ -147,7 +138,6 @@ Contract address is invalid Derivation path is invalid Please select the network - Required field Decimal must be a valid integer, up to %li Custom derivation E. g. m/00\'/0000\'/0\'/0/0 @@ -179,7 +169,6 @@ Before executing any command entailing a change of the card state, you will have to enter the passcode. Referral program Flip your device screen down to quickly hide and show balances - Privacy policy %s hashes Card ID Contact to Support @@ -191,14 +180,12 @@ Details Check your internet connection or switch to a different network Terms of Service - Oops, the current version of the application is not ready to work with this card, please check for updates. You have used a card from another wallet. Tap the card associated with this wallet My tokens You haven\'t added any tokens yet. Add tokens via Market to swap Cannot be swapped for %s Provided by Provided by %s - Additionally, the network fee for sending the exchanged funds back to your address is included in the rate Status Tangem offers token swaps via 3rd-party providers according to each provider\'s terms Choose provider @@ -266,18 +253,9 @@ To sign tap the card as shown above and do not remove until the end of the operation Tap to sign Tap the card - Internal error: wallet manager not found You have updated biometrics, scan your card to enter To begin tracking your crypto assets and transactions, add tokens - You have completed all of the lessons, and are now eligible to receive your 1INCH tokens - - Complete three lessons and receive %d 1INCH token to your wallet - Complete three lessons and receive %d 1INCH tokens to your wallet - Manage tokens - Total balance - The amount does not include some of your funds - 1INCH tokens will be credited to your %s wallet address within 48 hours To access all the networks you need to scan the card Scan your card Enjoy %1$s service fees on swaps via Changelly from February %2$s-%3$s @@ -292,7 +270,6 @@ Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk. Not original or primary blockchain the token is hosted Non-native networks - Networks Choose networks Wallet Couldn’t find this token, you can add it manually @@ -325,10 +302,8 @@ Scan the card #%d Backup now Scan the primary card - Claim Continue to my wallet Finalize the backup - Set PIN code Receive crypto Scan primary card Skip for later @@ -345,8 +320,6 @@ Do you want to exit the activation process? Getting started Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet? - Pin code - Connect Creating a backup Read more about seed phrase Write these 12 words down in the order given below and store them in a safe and secret place. @@ -361,23 +334,17 @@ Legacy To check whether you’ve written down your seed phrase correctly, please enter the 2nd, 7th and 11th words So, let’s check - To get started, simply claim wxDAI to your wallet - It will take a few seconds To start the backup process add up to two backup cards. You can add one more card or finalize the backup process Prepare the backup card with number %s Scan the primary card to start the backup process. Prepare the primary card with number %s - Congratulations! Your first payment crypto card has been activated! Your wallet card is configured and ready for use. Max number of cards added. Finalize the backup process. Activating card Backup card #%d - Claim %s - Claiming No backup cards One backup card added - PIN Code Prepare your card Two backup cards added To get started, simply top up the wallet with any amount @@ -423,8 +390,6 @@ for %d wallets Will get ^^%1$s^^ for each wallet bought by your friend on your %2$s network address %3$s ^^30 days after^^ that - Will get - for each wallet bought by your friend on your %1$s network address%2$s You Will get a when buying a wallet on tangem.com @@ -441,7 +406,6 @@ %d wallet %d wallets - Please hold the card until the operation complete Reset the Card I understand that after performing this action, I will no longer have access to the current wallet I realize that I can\'t use this card to recover my access code on the other cards of the current wallet @@ -458,7 +422,6 @@ Enable biometric authorization Would you like to use biometrics? Note that making a transaction with your funds will still require your card - Scan card failed. Try again Scan Card Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet. Get your card ready! @@ -466,7 +429,7 @@ Subtract Not enough funds to cover the network commission. Subtract the commission from the amount sent? Fee is increased - Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? + You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue? Reason: %1$s\nCode: %2$s The transaction is not completed Amount @@ -500,8 +463,6 @@ Network fee coverage Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance Total exceeds balance - The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance. - Fee exceeds balance The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s. Existential deposit The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. @@ -542,7 +503,6 @@ I have a promo code… Tangem Wallet Other payment methods - Pre-order now Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet Up to 3 physical cards to one wallet @@ -567,11 +527,9 @@ Error: %s You swap Give Permission - High price impact! Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Sending amount will be reduced to cover the selected fee level - Insufficient funds in your %1$s wallet to cover fees. Top up your %2$s wallet first. Transaction in progress... Waiting Approve @@ -580,27 +538,16 @@ Give Permission Specify the approve limit for the selected token Amount %s - Spender - Your wallet To continue, grant 1inch smart contracts permission to use your %s Unlimited - View in Explorer In progress Swap - Swap %s for - Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product. You receive - Other tokens Choose token - Your tokens not available Balances hidden Balances shown Undo - - %d token - %d tokens - Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. Hide %s @@ -608,7 +555,6 @@ %1$s token in %%image%% %2$s network The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list. Unable to hide %s - No rate Exchange this token for another at %1$s service fees from February %2$s-%3$s. Swap with Changelly, %s fees Swap now @@ -620,7 +566,6 @@ Operation from: %s to: %s - In progress… You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d You\'ve scanned wrong twin card. Please try another one This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. @@ -635,25 +580,15 @@ Use %s or scan a card to have an access to your wallet Add new wallet Are you sure you want to delete this wallet? - %d selected An error has occurred, please scan your card to log in This wallet has already been saved, you can add another one - Multi-currency Wallet name Rename Wallet - Single-currency - My Wallets Unlock all Unlock all with %s Transaction history Blockchain is unreachable. Try later - Balance is loading… Scan the card - Transaction is in progress… - Verified Balance - Actions - Choose wallet option - Do you want to buy or sell crypto? Requesting to sign a message.\n\n%s Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s Trade order for %1$s\nPrice: %2$s\nAmount to receive: %3$s\nAmount to pay: %4$s @@ -693,19 +628,11 @@ The transaction has been successfully signed and sent to the Dapp The transaction has been succesfully signed and sent to the blockchain Failed to find transaction hash - %s network - Account is not created - This card is not supported - Your Tangem card was made to work with a different application. Please see the name and instructions on your card, and install the correct app %s Market Price last 24h %s network Address was copied to clipboard No internet connection - Receiving - from %s - Sending - to %s Tangem Use %s or scan a card to unlock access to your wallet Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost. From 996356ef6121555ae8dce8e1b9bd7274e1c9ae7f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Mar 2024 12:44:46 +0000 Subject: [PATCH 007/109] Updated on 2026-08-14 --- app/build.gradle.kts | 3 +++ gradle/dependencies.toml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ee0bc5020b..ce615a8e11 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -230,6 +230,9 @@ dependencies { implementation(deps.listenableFuture) implementation(deps.mlKit.barcodeScanning) + /** Leakcanary */ + debugImplementation(deps.leakcanary) + /** Excluded dependencies */ implementation("com.google.guava:guava:30.0-android") { // excludes version 9999.0-empty-to-avoid-conflict-with-guava diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 4b9302bf6b..02f4d4bad6 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -82,6 +82,7 @@ listenableFuture = "1.0" swipeRefreshLayout = "1.1.0" spr-client = "3.6.2" web3j = "4.10.1" +leakcanary = "2.13" # endregion Other libraries # region Tangem @@ -249,4 +250,5 @@ camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "and camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidXCamera" } camera-view = { module = "androidx.camera:camera-view", version.ref = "androidXCamera" } web3j-core = { module = "org.web3j:core", version.ref = "web3j" } +leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } # endregion Other From af4a278e88747d3805215595e032cb64ec6a1618 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Mar 2024 11:54:19 +0100 Subject: [PATCH 008/109] Updated on 2026-08-14 --- app/build.gradle.kts | 4 - .../main/java/com/tangem/tap/MainActivity.kt | 24 +- .../java/com/tangem/tap/TapApplication.kt | 3 - .../converters/ShopOrderToEventConverter.kt | 28 -- .../tap/common/extensions/Navigation.kt | 2 - .../com/tangem/tap/common/redux/AppReducer.kt | 2 - .../com/tangem/tap/common/redux/AppState.kt | 4 - .../tap/common/shop/TangemShopService.kt | 222 ------------ .../tap/common/shop/data/TangemProduct.kt | 9 - .../tangem/tap/common/shop/data/TotalSum.kt | 6 - .../common/shop/googlepay/GooglePayService.kt | 147 -------- .../common/shop/googlepay/GooglePayUtil.kt | 133 ------- .../tap/common/shop/shopify/ShopifyService.kt | 267 -------------- .../tap/common/shop/shopify/data/Checkout.kt | 135 ------- .../common/shop/shopify/data/CheckoutItem.kt | 8 - .../common/shop/shopify/data/Collection.kt | 52 --- .../shop/data/DefaultShopRepository.kt | 59 --- .../shop/data/SalesProductConverter.kt | 40 --- .../tap/features/shop/di/ShopUseCaseModule.kt | 44 --- .../features/shop/di/ShopifyTogglesModule.kt | 23 -- ...faultShopifyOrderingAvailabilityUseCase.kt | 15 - .../domain/GetShopifySalesProductsUseCase.kt | 25 -- .../features/shop/domain/ShopRepository.kt | 17 - .../ShopifyOrderingAvailabilityUseCase.kt | 12 - .../shop/domain/models/ProductType.kt | 22 -- .../features/shop/domain/models/SalesError.kt | 5 - .../shop/domain/models/SalesProduct.kt | 30 -- .../shop/presentation/ShopViewModel.kt | 60 ---- .../tap/features/shop/redux/ShopAction.kt | 52 --- .../tap/features/shop/redux/ShopMiddleware.kt | 128 ------- .../tap/features/shop/redux/ShopReducer.kt | 49 --- .../tap/features/shop/redux/ShopState.kt | 28 -- .../DefaultShopifyFeatureToggleManager.kt | 11 - .../toggles/ShopifyFeatureToggleManager.kt | 10 - .../tap/features/shop/ui/ShopFragment.kt | 251 ------------- app/src/main/res/layout/fragment_shop.xml | 336 ------------------ .../datasource/config/ConfigManagerImpl.kt | 1 - .../tangem/datasource/config/models/Config.kt | 1 - .../datasource/config/models/JsonModels.kt | 1 - .../datasource/config/models/ShopifyShop.kt | 14 - .../configs/feature_toggles_config.json | 4 - .../tangem/core/navigation/NavigationState.kt | 1 - gradle/dependencies.toml | 2 - 43 files changed, 1 insertion(+), 2286 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/domain/DefaultShopifyOrderingAvailabilityUseCase.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/domain/ShopifyOrderingAvailabilityUseCase.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt delete mode 100644 app/src/main/res/layout/fragment_shop.xml delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/config/models/ShopifyShop.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ce615a8e11..0ca24d28ab 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -183,10 +183,6 @@ dependencies { implementation(deps.kotsonGson) implementation(deps.spongecastle.core) implementation(deps.lottie) - implementation(deps.shopify.buy) { - exclude(group = "com.shopify.graphql.support") - exclude(module = "joda-time") - } implementation(deps.compose.accompanist.appCompatTheme) implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.accompanist.webView) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 116e6dcf12..99a374a21f 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -52,9 +52,6 @@ import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.redux.NotificationsHandler -import com.tangem.tap.common.shop.googlepay.GooglePayService -import com.tangem.tap.common.shop.googlepay.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE -import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor @@ -64,7 +61,6 @@ import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHan import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.features.main.model.Toast import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction -import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.tap.features.welcome.ui.WelcomeFragment import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction @@ -173,8 +169,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac setContentView(R.layout.activity_main) initContent() - checkGooglePayAvailability() - checkForNotificationPermission() observeStateUpdates() @@ -256,14 +250,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } - private fun checkGooglePayAvailability() { - store.dispatch( - ShopAction.CheckIfGooglePayAvailable( - GooglePayService(createPaymentsClient(this), this), - ), - ) - } - private fun createAppThemeModeFlow(): SharedFlow { val tapApplication = application as TapApplication @@ -377,13 +363,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac super.onActivityResult(requestCode, resultCode, data) onActivityResultCallbacks.forEach { it(requestCode, resultCode, data) } - when (requestCode) { - LOAD_PAYMENT_DATA_REQUEST_CODE -> { - store.dispatch( - ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data), - ) - } - } } override fun addOnActivityResultCallback(callback: OnActivityResultCallback) { @@ -432,10 +411,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0 val isNotScannedBefore = store.state.globalState.scanResponse == null val isOnboardingServiceNotActive = store.state.globalState.onboardingState.onboardingStarted - val isShopNotOpened = store.state.shopState.total != null when { - !backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive && isShopNotOpened -> { + !backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> { navigateToInitialScreen(intentWhichStartedActivity) } backStackIsEmpty -> { diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index de7e20d595..7c26fe7570 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -59,7 +59,6 @@ import com.tangem.tap.common.log.TimberFormatStrategy import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.appReducer import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.common.shop.TangemShopService import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tasks.product.DerivationsFinder import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation @@ -84,7 +83,6 @@ lateinit var foregroundActivityObserver: ForegroundActivityObserver lateinit var activityResultCaller: ActivityResultCaller lateinit var preferencesStorage: PreferencesDataSource lateinit var walletConnectRepository: WalletConnectRepository -lateinit var shopService: TangemShopService internal lateinit var derivationsFinder: DerivationsFinder @HiltAndroidApp @@ -300,7 +298,6 @@ internal class TapApplication : Application(), ImageLoaderFactory { } private fun initWithConfigDependency(config: Config) { - shopService = TangemShopService(this, config.shopify!!) initAnalytics(this, config) initFeedbackManager(this, foregroundActivityObserver, store) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt deleted file mode 100644 index fd658029b7..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.tap.common.analytics.converters - -import com.shopify.buy3.Storefront -import com.tangem.tap.common.analytics.events.Shop -import com.tangem.tap.features.shop.domain.models.ProductType -import com.tangem.utils.converter.Converter - -/** -[REDACTED_AUTHOR] - */ -class ShopOrderToEventConverter : Converter, Shop.Purchased> { - - override fun convert(value: Pair): Shop.Purchased { - val checkout = value.first - val productType = value.second - - val sku = checkout.lineItems?.edges?.firstOrNull()?.node?.variant?.sku ?: productType.sku - val count = when (productType) { - ProductType.WALLET_2_CARDS -> "2" - ProductType.WALLET_3_CARDS -> "3" - } - val amount = "${checkout.totalPriceV2.amount} ${checkout.totalPriceV2.currencyCode.name}" - val code = (checkout.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication) - ?.code - - return Shop.Purchased(sku, count, amount, code) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 019b203988..f6c299acb1 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -24,7 +24,6 @@ import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragm import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment import com.tangem.tap.features.send.ui.SendFragment -import com.tangem.tap.features.shop.ui.ShopFragment import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment import com.tangem.tap.features.welcome.ui.WelcomeFragment import com.tangem.tap.proxy.redux.DaggerGraphState @@ -122,7 +121,6 @@ fun FragmentActivity.getPreviousScreen(): AppScreen? { private fun fragmentFactory(screen: AppScreen): Fragment { return when (screen) { AppScreen.Home -> HomeFragment() - AppScreen.Shop -> ShopFragment() AppScreen.OnboardingNote -> OnboardingNoteFragment() AppScreen.OnboardingWallet -> OnboardingWalletFragment() AppScreen.OnboardingTwins -> OnboardingTwinsFragment() diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index a1232dd031..cd51cff94e 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -12,7 +12,6 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer import com.tangem.tap.features.send.redux.reducers.SendScreenReducer -import com.tangem.tap.features.shop.redux.ShopReducer import com.tangem.tap.features.signin.redux.SignInReducer import com.tangem.tap.features.tokens.legacy.redux.TokensReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer @@ -37,7 +36,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) disclaimerState = DisclaimerReducer.reduce(action, state), tokensState = TokensReducer.reduce(action, state), walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState), - shopState = ShopReducer.reduce(action, state.shopState), welcomeState = WelcomeReducer.reduce(action, state), saveWalletState = SaveWalletReducer.reduce(action, state), signInState = SignInReducer.reduce(action, state), diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 892271058e..7cab6dbfbd 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -29,8 +29,6 @@ import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware import com.tangem.tap.features.saveWallet.redux.SaveWalletState import com.tangem.tap.features.send.redux.middlewares.SendMiddleware import com.tangem.tap.features.send.redux.states.SendState -import com.tangem.tap.features.shop.redux.ShopMiddleware -import com.tangem.tap.features.shop.redux.ShopState import com.tangem.tap.features.signin.redux.SignInMiddleware import com.tangem.tap.features.signin.redux.SignInState import com.tangem.tap.features.tokens.legacy.redux.TokensState @@ -56,7 +54,6 @@ data class AppState( val disclaimerState: DisclaimerState = DisclaimerState(), val tokensState: TokensState = TokensState(), val walletConnectState: WalletConnectState = WalletConnectState(), - val shopState: ShopState = ShopState(), val welcomeState: WelcomeState = WelcomeState(), val saveWalletState: SaveWalletState = SaveWalletState(), val signInState: SignInState = SignInState(), @@ -92,7 +89,6 @@ data class AppState( DisclaimerMiddleware().disclaimerMiddleware, WalletConnectMiddleware().walletConnectMiddleware, BackupMiddleware().backupMiddleware, - ShopMiddleware().shopMiddleware, WelcomeMiddleware().middleware, SaveWalletMiddleware().middleware, LockUserWalletsTimerMiddleware().middleware, diff --git a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt deleted file mode 100644 index 1a1b5827f0..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt +++ /dev/null @@ -1,222 +0,0 @@ -package com.tangem.tap.common.shop - -import android.app.Application -import android.content.Intent -import com.google.android.gms.wallet.PaymentData -import com.shopify.buy3.Storefront -import com.tangem.core.analytics.Analytics -import com.tangem.datasource.config.models.ShopifyShop -import com.tangem.tap.common.analytics.converters.ShopOrderToEventConverter -import com.tangem.tap.common.extensions.filterNotNull -import com.tangem.tap.common.shop.data.TangemProduct -import com.tangem.tap.common.shop.data.TotalSum -import com.tangem.tap.common.shop.googlepay.GooglePayService -import com.tangem.tap.common.shop.shopify.ShopifyService -import com.tangem.tap.common.shop.shopify.data.CheckoutItem -import com.tangem.tap.features.shop.domain.models.ProductType -import com.tangem.tap.features.shop.domain.models.ProductType.Companion.SKUS_TO_DISPLAY -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import java.math.BigDecimal -import java.util.Currency -import java.util.UUID - -class TangemShopService(application: Application, shopifyShop: ShopifyShop) { - - private val shopifyService = ShopifyService(application, shopifyShop) - - private val checkouts = mutableMapOf() - private val variants = mutableMapOf() - - private lateinit var googlePayService: GooglePayService - - suspend fun getProducts(): Result> { - val result = shopifyService.getProducts() - - return result.mapCatching { product -> - val availableVariants = product - .flatMap { it.variants.edges.map { it.node } } - .filter { SKUS_TO_DISPLAY.contains(it.sku) } - .associateBy { ProductType.fromSku(it.sku) } - .filterNotNull() - variants.putAll(availableVariants) - - if (variants.size < SKUS_TO_DISPLAY.size) { - return Result.failure( - Exception( - "Shopify: products are missing, " + - "\nproducts available: ${variants.keys.map { it.sku }}", - ), - ) - } - - val twoCardsProduct = TangemProduct( - type = ProductType.WALLET_2_CARDS, - totalSum = TotalSum( - finalValue = variants[ProductType.WALLET_2_CARDS]?.priceV2?.format(), - beforeDiscount = variants[ProductType.WALLET_2_CARDS]?.compareAtPriceV2?.format(), - ), - ) - val threeCardsProduct = TangemProduct( - type = ProductType.WALLET_3_CARDS, - totalSum = TotalSum( - finalValue = variants[ProductType.WALLET_3_CARDS]?.priceV2?.format(), - beforeDiscount = variants[ProductType.WALLET_3_CARDS]?.compareAtPriceV2?.format(), - ), - ) - createCheckouts() - return Result.success(listOf(twoCardsProduct, threeCardsProduct)) - } - } - - private suspend fun createCheckouts() { - variants.keys.map { coroutineScope { async { createCheckout(it) } } }.awaitAll() - } - - private suspend fun createCheckout(productType: ProductType) { - val checkoutItem = CheckoutItem(variants[productType]!!.id, 1) - val result = shopifyService.createCheckout(listOf(checkoutItem)) - result.onSuccess { checkout -> - checkouts[productType] = checkout - } - } - - suspend fun checkIfGooglePayAvailable(googlePayService: GooglePayService): Result { - this.googlePayService = googlePayService - return googlePayService.checkIfGooglePayAvailable(shopifyService.shop.merchantID != null) - } - - fun buyWithGooglePay(productType: ProductType) { - shopifyService.shop.merchantID?.let { merchantId -> - val totalPrice = checkouts[productType]!!.totalPriceV2.amount - googlePayService.payWithGooglePay( - totalPriceCents = totalPrice, - currencyCode = checkouts[productType]!!.currencyCode.name, - merchantID = merchantId, - ) - } - } - -// fun subscribeToGooglePayResult( -// productType: ProductType, -// resultCallback: (Result) -> Unit -// ) { -// googlePayService.responseCallback = { result -> -// result.onFailure { } -// result.onSuccess { -// completeTokenizedPayment(it, productType) -// } -// } -// } - - suspend fun handleGooglePayResult(resultCode: Int, data: Intent?, productType: ProductType): Result { - val result = googlePayService.handleResponseFromGooglePay(resultCode, data) - result.onSuccess { - val finalizePaymentResult = completeTokenizedPayment(it, productType) - finalizePaymentResult.onSuccess { - return Result.success(Unit) - } - return Result.failure(finalizePaymentResult.exceptionOrNull()!!) - } - return Result.failure(result.exceptionOrNull()!!) - } - - private suspend fun completeTokenizedPayment( - paymentData: PaymentData, - productType: ProductType, - ): Result { - val checkout = checkouts[productType]!! - val googlePayResponse = - googlePayService.parsePaymentData(paymentData) - ?: return Result.failure(Exception("cannot parse GPay result")) - - val amount = - Storefront.MoneyInput(checkout.totalPriceV2.amount, checkout.totalPriceV2.currencyCode) - val idempotencyKey = UUID.randomUUID().toString() - val addressGPay = googlePayResponse.billingAddress - - val address = Storefront.MailingAddressInput().apply { - lastName = addressGPay.name - address1 = addressGPay.address1 - address2 = addressGPay.address2 + addressGPay.address3 - province = addressGPay.administrativeArea - zip = addressGPay.postalCode - phone = addressGPay.phoneNumber - } - - val payment = Storefront.TokenizedPaymentInputV3( - amount, - idempotencyKey, - address, - paymentData.toJson(), - Storefront.PaymentTokenType.GOOGLE_PAY, - ) - .setTest(true) - - return shopifyService.completeWithTokenizedPayment( - payment = payment, - checkoutID = checkout.id, - ) - } - - suspend fun applyPromoCode(promoCode: String): Result> { - val products = variants.keys - .map { coroutineScope { async { applyPromoCode(promoCode, it) } } } - .awaitAll() - .map { result -> result.getOrElse { return Result.failure(it) } } - - return Result.success(products) - } - - private suspend fun applyPromoCode(promoCode: String, productType: ProductType): Result { - val checkout = checkouts[productType] ?: return Result.failure(Exception("No checkout")) - - val result = if (promoCode.isBlank()) { - shopifyService.removeDiscount(checkout.id) - } else { - shopifyService.applyDiscount(promoCode, checkout.id) - } - - result.onSuccess { - checkouts[productType] = it - return Result.success( - TangemProduct( - productType, - TotalSum( - finalValue = it.totalPriceV2.format(), - beforeDiscount = variants[productType]!!.compareAtPriceV2.format(), - ), - appliedDiscount = it.getAppliedDiscount(), - ), - ) - } - return Result.failure(result.exceptionOrNull()!!) - } - - fun getCheckoutUrl(productType: ProductType): String { - return checkouts[productType]!!.webUrl - } - - suspend fun waitForCheckout(productType: ProductType) { - val result = shopifyService.checkout(true, checkouts[productType]!!.id) - result.onSuccess { checkout -> - if (checkout.order != null && checkout.lineItems != null) { - val event = ShopOrderToEventConverter().convert(checkout to productType) - Analytics.send(event) - } - } - } -} - -private fun Storefront.MoneyV2.format(): String { - val currencySymbol = Currency.getInstance(currencyCode.name).symbol - val amountFormatted = BigDecimal(amount).setScale(2) - return currencySymbol + amountFormatted -} - -private fun Storefront.Checkout.getAppliedDiscount(): String? { - val discountApplication = - discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication - return discountApplication?.code -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt b/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt deleted file mode 100644 index 9a01a5df51..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.common.shop.data - -import com.tangem.tap.features.shop.domain.models.ProductType - -data class TangemProduct( - val type: ProductType, - val totalSum: TotalSum? = null, - val appliedDiscount: String? = null, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt b/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt deleted file mode 100644 index a9e920745f..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.tap.common.shop.data - -data class TotalSum( - val finalValue: String? = null, - val beforeDiscount: String? = null, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt deleted file mode 100644 index 7526224414..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.tangem.tap.common.shop.googlepay - -import android.app.Activity -import android.app.Activity.RESULT_CANCELED -import android.app.Activity.RESULT_OK -import android.content.Intent -import android.util.Log -import com.google.android.gms.common.api.ApiException -import com.google.android.gms.wallet.* -import com.tangem.common.core.TangemSdkError -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.json.JSONException -import org.json.JSONObject -import timber.log.Timber -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -class GooglePayService(private val paymentsClient: PaymentsClient, private val activity: Activity) { - -// var responseCallback: ((Result) -> Unit)? = null - - suspend fun checkIfGooglePayAvailable(isMerchantAvailable: Boolean): Result { - if (!isMerchantAvailable) return Result.success(false) - val isReadyToPayJson = GooglePayUtil.isReadyToPayRequest() ?: return Result.success(false) - val request = IsReadyToPayRequest.fromJson(isReadyToPayJson.toString()) - - val task = paymentsClient.isReadyToPay(request) - return withContext(Dispatchers.IO) { - suspendCoroutine { continuation -> - task.addOnCompleteListener { completedTask -> - try { - val result = completedTask.getResult(ApiException::class.java) - continuation.resume(Result.success(true)) - } catch (exception: ApiException) { - // Process error - Timber.w("isReadyToPay failed: $exception") - continuation.resume(Result.failure(exception)) - } - } - } - } - } - - fun payWithGooglePay(totalPriceCents: String, currencyCode: String, merchantID: String) { - val paymentDataRequestJson = GooglePayUtil.getPaymentDataRequest( - totalPriceCents, - currencyCode = currencyCode, - countryCode = "RU", - merchantID = merchantID, - ) - if (paymentDataRequestJson == null) { - Timber.e("RequestPayment: can't fetch payment data request") - return - } - val request = PaymentDataRequest.fromJson(paymentDataRequestJson.toString()) - - AutoResolveHelper.resolveTask( - paymentsClient.loadPaymentData(request), - activity, - LOAD_PAYMENT_DATA_REQUEST_CODE, - ) - } - - fun handleResponseFromGooglePay(resultCode: Int, data: Intent?): Result { - val result = when (resultCode) { - RESULT_OK -> { - val paymentData = data?.let { intent -> PaymentData.getFromIntent(intent) } - if (paymentData == null) { - Result.failure(Exception("No payment data")) - } else { - Result.success(paymentData) - } - } - RESULT_CANCELED -> { - Result.failure(TangemSdkError.UserCancelled()) - } - AutoResolveHelper.RESULT_ERROR -> { - val statusCode = AutoResolveHelper.getStatusFromIntent(data)?.statusCode - if (statusCode == null) { - Result.failure(Exception("Unknown Status")) - } else { - Result.failure(Exception("$statusCode")) - } - } - else -> Result.failure(Exception("Unknown Status")) - } -// responseCallback?.invoke(result) - return result - } - - fun parsePaymentData(paymentData: PaymentData): GooglePayResponse? { - val paymentInformation = paymentData.toJson() - - try { - // Token will be null if PaymentDataRequest was not constructed using fromJson(String). - val paymentMethodData = - JSONObject(paymentInformation).getJSONObject("paymentMethodData") - val addressJson = paymentMethodData.getJSONObject("info") - .getJSONObject("billingAddress") - - val address = Address( - name = addressJson.getString("name"), - postalCode = addressJson.getString("postalCode"), - countryCode = addressJson.getString("countryCode"), - phoneNumber = addressJson.getString("phoneNumber"), - address1 = addressJson.getString("address1"), - address2 = addressJson.getString("address2"), - address3 = addressJson.getString("address3"), - locality = addressJson.getString("locality"), - administrativeArea = addressJson.getString("administrativeArea"), - sortingCode = addressJson.getString("sortingCode"), - ) - - val token = paymentMethodData - .getJSONObject("tokenizationData") - .getString("token") - - return GooglePayResponse(address, token) - } catch (e: JSONException) { - Log.e("handlePaymentSuccess", "Error: " + e.toString()) - } - return null - } - - companion object { - const val LOAD_PAYMENT_DATA_REQUEST_CODE = 315 - } -} - -data class GooglePayResponse( - val billingAddress: Address, - val token: String, -) - -data class Address( - val name: String, - val postalCode: String, - val countryCode: String, - val phoneNumber: String, - val address1: String, - val address2: String, - val address3: String, - val locality: String, - val administrativeArea: String, - val sortingCode: String, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt deleted file mode 100644 index 1e5cfba714..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.tangem.tap.common.shop.googlepay - -import android.app.Activity -import com.google.android.gms.wallet.PaymentsClient -import com.google.android.gms.wallet.Wallet -import com.google.android.gms.wallet.WalletConstants -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject - -object GooglePayUtil { - private val baseRequest = JSONObject().apply { - put("apiVersion", 2) - put("apiVersionMinor", 0) - } - - @Suppress("MagicNumber") - private val allowedCardNetworks = JSONArray( - listOf( - "AMEX", - "DISCOVER", - "INTERAC", - "JCB", - "MASTERCARD", - "VISA", - ), - ) - - private val allowedCardAuthMethods = JSONArray( - listOf( - "PAN_ONLY", - "CRYPTOGRAM_3DS", - ), - ) - - private val merchantInfo: JSONObject = JSONObject().put("merchantName", "Example Merchant") - - private fun gatewayTokenizationSpecification(merchantID: String): JSONObject { - return JSONObject().apply { - put("type", "PAYMENT_GATEWAY") - put( - "parameters", - JSONObject( - mapOf( - "gateway" to "shopify", - "gatewayMerchantId" to merchantID, - ), - ), - ) - } - } - - private fun baseCardPaymentMethod(): JSONObject { - return JSONObject().apply { - val parameters = JSONObject().apply { - put("allowedAuthMethods", allowedCardAuthMethods) - put("allowedCardNetworks", allowedCardNetworks) - put("billingAddressRequired", true) - put( - "billingAddressParameters", - JSONObject().apply { - put("format", "FULL") - }, - ) - } - - put("type", "CARD") - put("parameters", parameters) - } - } - - private fun cardPaymentMethod(merchantID: String): JSONObject { - val cardPaymentMethod = baseCardPaymentMethod() - cardPaymentMethod.put("tokenizationSpecification", gatewayTokenizationSpecification(merchantID)) - - return cardPaymentMethod - } - - fun createPaymentsClient(activity: Activity): PaymentsClient { - val walletOptions = Wallet.WalletOptions.Builder() - .setEnvironment(PAYMENTS_ENVIRONMENT) - .build() - - return Wallet.getPaymentsClient(activity, walletOptions) - } - - fun isReadyToPayRequest(): JSONObject? { - return try { - baseRequest.apply { - put("allowedPaymentMethods", JSONArray().put(baseCardPaymentMethod())) - } - } catch (e: JSONException) { - null - } - } - - private fun getTransactionInfo(price: String, countryCode: String, currencyCode: String): JSONObject { - return JSONObject().apply { - put("totalPrice", price) - put("totalPriceStatus", "FINAL") - put("countryCode", countryCode) - put("currencyCode", currencyCode) - } - } - - fun getPaymentDataRequest( - price: String, - countryCode: String, - currencyCode: String, - merchantID: String, - ): JSONObject? { - try { - return baseRequest.apply { - put("allowedPaymentMethods", JSONArray().put(cardPaymentMethod(merchantID))) - put("transactionInfo", getTransactionInfo(price, countryCode, currencyCode)) - put("merchantInfo", merchantInfo) - - // An optional shipping address requirement is a top-level property of the - // PaymentDataRequest JSON object. - val shippingAddressParameters = JSONObject().apply { - put("phoneNumberRequired", false) -// put("allowedCountryCodes", JSONArray(listOf("US", "GB"))) - } - put("shippingAddressParameters", shippingAddressParameters) - put("shippingAddressRequired", true) - } - } catch (e: JSONException) { - return null - } - } -} - -const val PAYMENTS_ENVIRONMENT = WalletConstants.ENVIRONMENT_TEST \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt deleted file mode 100644 index 705bd067a0..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt +++ /dev/null @@ -1,267 +0,0 @@ -package com.tangem.tap.common.shop.shopify - -import android.app.Application -import com.shopify.buy3.GraphCallResult -import com.shopify.buy3.GraphClient -import com.shopify.buy3.RetryHandler -import com.shopify.buy3.Storefront.* -import com.shopify.graphql.support.ID -import com.shopify.graphql.support.Input -import com.tangem.datasource.config.models.ShopifyShop -import com.tangem.tap.common.shop.shopify.data.CheckoutItem -import com.tangem.tap.common.shop.shopify.data.checkoutFieldsFragment -import com.tangem.tap.common.shop.shopify.data.collectionFieldsFragment -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.util.concurrent.TimeUnit -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -@Suppress("LargeClass") -class ShopifyService(private val application: Application, val shop: ShopifyShop) { - private val client: GraphClient by lazy { initClient() } - - @Suppress("MagicNumber") - suspend fun getProducts(collectionTitleFilter: String? = null): Result> { - val filter = collectionTitleFilter?.let { "title:\"$it\"" } - - val query = query { rootQuery: QueryRootQuery -> - rootQuery.collections( - { arg -> arg.first(250).query(filter) }, - CollectionConnectionQuery::collectionFieldsFragment, - ) - } - return when (val result = queryAsync(query)) { - is GraphCallResult.Success -> { - val products = result.response.data!!.collections.edges - .map { it.node.products } - .flatMap { it.edges } - .map { it.node } - Result.success(products) - } - is GraphCallResult.Failure -> { - Result.failure(result.error) - } - } - } - - suspend fun checkout(pollUntilOrder: Boolean, checkoutID: ID): Result { - val query = query { rootQuery: QueryRootQuery -> - rootQuery - .node(checkoutID) { query -> - query.onCheckout { checkoutQuery -> - with(checkoutQuery) { - checkoutFieldsFragment() - } - } - } - } - val retryHandler = RetryHandler.build( - delay = 1, - timeUnit = TimeUnit.SECONDS, - ) { - this.retryWhen { result -> - when (result) { - is GraphCallResult.Success -> { - val checkout = result.response.data?.node as? Checkout - checkout?.order == null - } - is GraphCallResult.Failure -> false - } - } - } - - val result = if (pollUntilOrder) queryAsync(query, retryHandler) else queryAsync(query) - return when (result) { - is GraphCallResult.Success -> { - val checkout = result.response.data!!.node as? Checkout - if (checkout != null) { - Result.success(checkout) - } else { - Result.failure(ShopifyError.Unknown) - } - } - is GraphCallResult.Failure -> { - Result.failure(result.error) - } - } - } - - suspend fun createCheckout(checkoutItems: List, checkoutID: ID? = null): Result { - val storefrontLineItems: MutableList = checkoutItems - .map { CheckoutLineItemInput(it.quantity, it.id) }.toMutableList() - - val query = if (checkoutID != null) { - mutation { mutationQuery: MutationQuery -> - mutationQuery - .checkoutLineItemsReplace( - storefrontLineItems, - checkoutID, - ) { payloadQuery: CheckoutLineItemsReplacePayloadQuery -> - payloadQuery - .checkout { checkoutQuery: CheckoutQuery -> - checkoutQuery.checkoutFieldsFragment() - } - .userErrors { userErrorQuery: CheckoutUserErrorQuery -> - userErrorQuery - .field() - .message() - } - } - } - } else { - val input = CheckoutCreateInput() - .setLineItemsInput( - Input.value(storefrontLineItems), - ) - mutation { mutationQuery: MutationQuery -> - mutationQuery - .checkoutCreate( - input, - ) { payloadQuery: CheckoutCreatePayloadQuery -> - payloadQuery - .checkout { checkoutQuery: CheckoutQuery -> - checkoutQuery.checkoutFieldsFragment() - } - .checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery -> - userErrorQuery - .field() - .message() - } - } - } - } - return runCheckoutMutation(query) - } - - suspend fun applyDiscount(discountCode: String, checkoutID: ID): Result { - val query = mutation { mutationQuery: MutationQuery -> - mutationQuery - .checkoutDiscountCodeApplyV2( - discountCode, - checkoutID, - ) { payloadQuery: CheckoutDiscountCodeApplyV2PayloadQuery -> - payloadQuery - .checkout { checkoutQuery: CheckoutQuery -> - checkoutQuery.checkoutFieldsFragment() - } - .checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery -> - userErrorQuery - .field() - .message() - } - } - } - return runCheckoutMutation(query) - } - - suspend fun removeDiscount(checkoutID: ID): Result { - val query = mutation { mutationQuery: MutationQuery -> - mutationQuery - .checkoutDiscountCodeRemove( - checkoutID, - ) { payloadQuery: CheckoutDiscountCodeRemovePayloadQuery -> - payloadQuery - .checkout { checkoutQuery: CheckoutQuery -> - checkoutQuery.checkoutFieldsFragment() - } - .checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery -> - userErrorQuery - .field() - .message() - } - } - } - return runCheckoutMutation(query) - } - - suspend fun completeWithTokenizedPayment(payment: TokenizedPaymentInputV3, checkoutID: ID): Result { - val query = mutation { mutationQuery: MutationQuery -> - mutationQuery - .checkoutCompleteWithTokenizedPaymentV3( - checkoutID, - payment, - ) { payloadQuery: CheckoutCompleteWithTokenizedPaymentV3PayloadQuery -> - payloadQuery - .payment { paymentQuery: PaymentQuery -> - paymentQuery - .ready() - .errorMessage() - } - .checkout { checkoutQuery: CheckoutQuery -> - checkoutQuery - .ready() - } - .checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery -> - userErrorQuery - .field() - .message() - } - } - } - return runCheckoutMutation(query) - } - - private suspend fun runCheckoutMutation(mutation: MutationQuery): Result { - return when (val result = mutationQueryAsync(mutation)) { - is GraphCallResult.Success -> { - val checkout = result.response.data!!.checkoutCreate?.checkout - ?: result.response.data!!.checkoutDiscountCodeApplyV2?.checkout - ?: result.response.data!!.checkoutDiscountCodeRemove?.checkout - ?: result.response.data!!.checkoutShippingAddressUpdateV2?.checkout - ?: result.response.data!!.checkoutEmailUpdateV2?.checkout - ?: result.response.data!!.checkoutShippingLineUpdate?.checkout - ?: result.response.data!!.checkoutCompleteWithTokenizedPaymentV3.checkout - - Result.success(checkout) - } - is GraphCallResult.Failure -> Result.failure(result.error) - } - } - - private suspend fun queryAsync( - query: QueryRootQuery, - retryHandler: RetryHandler, - ): GraphCallResult = withContext(Dispatchers.IO) { - suspendCoroutine { continuation -> - client.queryGraph(query).enqueue(retryHandler = retryHandler) { result -> - continuation.resume(result) - } - } - } - - private suspend fun queryAsync(query: QueryRootQuery): GraphCallResult = withContext(Dispatchers.IO) { - suspendCoroutine { continuation -> - client.queryGraph(query).enqueue { result -> - continuation.resume(result) - } - } - } - - private suspend fun mutationQueryAsync(query: MutationQuery): GraphCallResult = - withContext(Dispatchers.IO) { - suspendCoroutine { continuation -> - client.mutateGraph(query).enqueue { result -> - continuation.resume(result) - } - } - } - - private fun initClient(): GraphClient { - return GraphClient.build( - application, - shop.domain, - shop.storefrontApiKey, - ) { -// httpCache(application.filesDir) { -// cacheMaxSizeBytes = (1024 * 1024 * 10) -// defaultCachePolicy = -// HttpCachePolicy.Default.CACHE_FIRST.expireAfter(20, TimeUnit.MINUTES) -// } - } - } -} - -sealed class ShopifyError : Throwable() { - object Unknown : ShopifyError() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt deleted file mode 100644 index 7565db2447..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.tangem.tap.common.shop.shopify.data - -import com.shopify.buy3.Storefront - -@Suppress("LongMethod", "MagicNumber") -fun Storefront.CheckoutQuery.checkoutFieldsFragment() { - ready() - webUrl() - currencyCode() - lineItemsSubtotalPrice { it.amount() } - totalPriceV2 { - it.currencyCode() - it.amount() - } - lineItems({ arg -> arg.first(250) }) { - it.edges { - it.node { -// it.id() - it.title() - it.quantity() - it.variant { - it.priceV2 { it.amount() } - } - } - } - } - shippingLine { - it.handle() - it.title() - it.priceV2 { it.amount() } - } - availableShippingRates { - it.ready() - it.shippingRates { - it.handle() - it.title() - it.priceV2 { it.amount() } - } - } - shippingAddress { - it.address1() - it.address2() - it.city() -// .company() - it.country() -// .countryCodeV2() - it.firstName() -// .formatted() -// .formattedArea() -// .id() - it.lastName() -// .latitude() -// .longitude() -// .name() - it.phone() - it.province() -// .provinceCode() - it.zip() - } - discountApplications({ arg -> arg.first(250) }) { - it.edges { - it.node { - it.onDiscountCodeApplication { - it.code() -// .applicable() -// .allocationMethod() -// .targetSelection() -// .targetType() - it.value { - it.onMoneyV2 { - it.amount() - } - it.onPricingPercentageValue { - it.percentage() - } - } - } - } - } - } - order { - it.cancelReason() - it.canceledAt() - it.currencyCode() -// .currentSubtotalPrice() -// .currentTotalDuties() -// .currentTotalPrice() -// .currentTotalTax() - it.customerLocale() - it.customerUrl() -// .discountApplications() - it.edited() - it.email() - it.financialStatus() - it.fulfillmentStatus() -// .id() -// .lineItems() -// .metafield() -// .metafields() - it.name() - it.orderNumber() -// .originalTotalDuties() -// .originalTotalPrice() - it.phone() - it.processedAt() - it.shippingAddress { - it.address1() - it.address2() - it.city() - it.company() - it.country() - it.countryCodeV2() - it.firstName() - it.formatted() - it.formattedArea() -// .id() - it.lastName() - it.latitude() - it.longitude() - it.name() - it.phone() - it.province() - it.provinceCode() - it.zip() - } -// .shippingDiscountAllocations() - it.statusUrl() -// .subtotalPriceV2() -// .successfulFulfillments() -// .totalPriceV2() -// .totalRefundedV2() -// .totalShippingPriceV2() -// .totalTaxV2() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt deleted file mode 100644 index 6d16e66908..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.tap.common.shop.shopify.data - -import com.shopify.graphql.support.ID - -data class CheckoutItem( - val id: ID, - val quantity: Int, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt deleted file mode 100644 index 1527fc9558..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.tap.common.shop.shopify.data - -import com.shopify.buy3.Storefront - -@Suppress("MagicNumber") -fun Storefront.CollectionConnectionQuery.collectionFieldsFragment() { - edges { collectionEdgeQuery -> - collectionEdgeQuery - .node { collectionQuery -> - collectionQuery - .title() - .products( - { arg -> arg.first(250) }, - ) { productConnectionQuery -> - productConnectionQuery - .edges { productEdgeQuery -> - productEdgeQuery - .node { productQuery -> - productQuery.title() - .productType() - .description() - .variants({ arg -> arg.first(10) }) { variantConnectionQuery -> - variantConnectionQuery.edges { variantQuery -> - variantQuery.node { - it.title() - it.sku() - it.currentlyNotInStock() - it.priceV2 { - it.amount() - it.currencyCode() - } - it.compareAtPriceV2 { - it.amount() - it.currencyCode() - } - it.compareAtPriceV2 { - it.amount() - } - it.product { - it.title() - .productType() - .description() - } - } - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt b/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt deleted file mode 100644 index 1abbd3d17c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.tangem.tap.features.shop.data - -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.ShopResponse -import com.tangem.domain.common.extensions.withIOContext -import com.tangem.tap.features.shop.domain.ShopRepository -import com.tangem.tap.features.shop.domain.models.SalesProduct -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching -import timber.log.Timber -import java.util.Locale - -/** - * Default implementation of shop feature repository - * - * @property tangemTechApi TangemTech API - * @property dispatchers coroutine dispatchers provider - * -[REDACTED_AUTHOR] - */ -internal class DefaultShopRepository( - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, -) : ShopRepository { - - private val salesProductConverter = SalesProductConverter() - - override suspend fun isShopifyOrderingAvailable(): Boolean { - return runCatching(dispatchers.io) { tangemTechApi.getShopInfo(name = SHOPIFY_NAME) } - .fold( - onSuccess = ShopResponse::isOrderingAvailable, - onFailure = { - Timber.e("Server error. isShopifyOrderingAvailable returns default value (true)") - true - }, - ) - } - - override suspend fun getSalesProductInfo(): List { - return withIOContext { - val salesInfo = tangemTechApi.getSalesInfo(locale = getLocaleName(), shops = SHOPIFY_NAME) - salesProductConverter.convert(salesInfo) - } - } - - private fun getLocaleName(): String { - return if (Locale.getDefault().language == "ru") { - RU_LOCALE - } else { - EN_LOCALE - } - } - - private companion object { - private const val SHOPIFY_NAME = "shopify" - private const val RU_LOCALE = "ru" - private const val EN_LOCALE = "en" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt b/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt deleted file mode 100644 index 9d8f440da6..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.tap.features.shop.data - -import com.tangem.datasource.api.tangemTech.models.SalesResponse -import com.tangem.tap.features.shop.domain.models.Notification -import com.tangem.tap.features.shop.domain.models.ProductState -import com.tangem.tap.features.shop.domain.models.ProductType -import com.tangem.tap.features.shop.domain.models.SalesProduct -import com.tangem.utils.converter.Converter - -internal class SalesProductConverter : Converter> { - - override fun convert(value: SalesResponse): List { - return value.sales.map { sales -> - val productState = when (sales.state) { - "order" -> ProductState.ORDER - "pre-order" -> ProductState.PRE_ORDER - "sold-out" -> ProductState.SOLD_OUT - else -> ProductState.SOLD_OUT - } - val productType = when (sales.product.code) { - "pack2" -> ProductType.WALLET_2_CARDS - "pack3" -> ProductType.WALLET_3_CARDS - else -> ProductType.WALLET_3_CARDS - } - SalesProduct( - id = sales.id, - productType = productType, - state = productState, - name = sales.product.name, - notification = sales.notification?.let { notification -> - Notification( - type = notification.type, - title = notification.title, - description = notification.description, - ) - }, - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt b/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt deleted file mode 100644 index a6f22bec79..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.tap.features.shop.di - -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.tap.features.shop.data.DefaultShopRepository -import com.tangem.tap.features.shop.domain.DefaultShopifyOrderingAvailabilityUseCase -import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase -import com.tangem.tap.features.shop.domain.ShopRepository -import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -internal object ShopUseCaseModule { - - @Provides - @ViewModelScoped - fun provideShopifyOrderingAvailabilityUseCase(shopRepository: ShopRepository): ShopifyOrderingAvailabilityUseCase { - return DefaultShopifyOrderingAvailabilityUseCase( - shopRepository = shopRepository, - ) - } - - @Provides - @ViewModelScoped - fun provideGetShopifySalesProductsUseCase(shopRepository: ShopRepository): GetShopifySalesProductsUseCase { - return GetShopifySalesProductsUseCase( - shopRepository = shopRepository, - ) - } - - @Provides - @ViewModelScoped - fun provideDefaultShopRepository( - tangemTechApi: TangemTechApi, - dispatchers: CoroutineDispatcherProvider, - ): ShopRepository { - return DefaultShopRepository(tangemTechApi = tangemTechApi, dispatchers = dispatchers) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt b/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt deleted file mode 100644 index 97b5839f47..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.tap.features.shop.di - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.tap.features.shop.toggles.DefaultShopifyFeatureToggleManager -import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object ShopifyTogglesModule { - - @Provides - @Singleton - fun provideDefaultShopifyFeatureToggleManager( - featureToggleManager: FeatureTogglesManager, - ): ShopifyFeatureToggleManager { - return DefaultShopifyFeatureToggleManager(featureToggleManager) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/DefaultShopifyOrderingAvailabilityUseCase.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/DefaultShopifyOrderingAvailabilityUseCase.kt deleted file mode 100644 index 41bbe3d449..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/DefaultShopifyOrderingAvailabilityUseCase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.tap.features.shop.domain - -/** - * Default implementation of use case to define shopify ordering availability - * - * @property shopRepository shop feature repository - * -[REDACTED_AUTHOR] - */ -internal class DefaultShopifyOrderingAvailabilityUseCase( - private val shopRepository: ShopRepository, -) : ShopifyOrderingAvailabilityUseCase { - - override suspend fun invoke() = shopRepository.isShopifyOrderingAvailable() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt deleted file mode 100644 index 7881b82dea..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.tap.features.shop.domain - -import arrow.core.Either -import arrow.core.left -import arrow.core.right -import com.tangem.tap.features.shop.domain.models.SalesError -import com.tangem.tap.features.shop.domain.models.SalesProduct - -/** - * Use case to get shopify available products - * - * @property shopRepository shop feature repository - */ -class GetShopifySalesProductsUseCase( - private val shopRepository: ShopRepository, -) { - - suspend operator fun invoke(): Either> { - return try { - shopRepository.getSalesProductInfo().right() - } catch (e: Exception) { - SalesError.DataError(e).left() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt deleted file mode 100644 index 67803630b8..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.tap.features.shop.domain - -import com.tangem.tap.features.shop.domain.models.SalesProduct - -/** - * Shop feature repository - * -[REDACTED_AUTHOR] - */ -interface ShopRepository { - - /** Get shopify ordering availability */ - suspend fun isShopifyOrderingAvailable(): Boolean - - /** Get actual sales product info */ - suspend fun getSalesProductInfo(): List -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopifyOrderingAvailabilityUseCase.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopifyOrderingAvailabilityUseCase.kt deleted file mode 100644 index 5f6c351ce2..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopifyOrderingAvailabilityUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.tap.features.shop.domain - -/** - * Use case to define shopify ordering availability - * -[REDACTED_AUTHOR] - */ -internal interface ShopifyOrderingAvailabilityUseCase { - - /** Get availability */ - suspend operator fun invoke(): Boolean -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt deleted file mode 100644 index 0987fdd8bd..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.tap.features.shop.domain.models - -private const val TANGEM_WALLET_2_CARDS_SKU = "TG115X2-S" -private const val TANGEM_WALLET_3_CARDS_SKU = "TG115X3-S" - -enum class ProductType(val sku: String) { - - WALLET_2_CARDS(TANGEM_WALLET_2_CARDS_SKU), - WALLET_3_CARDS(TANGEM_WALLET_3_CARDS_SKU), - ; - - companion object { - val SKUS_TO_DISPLAY = listOf(TANGEM_WALLET_2_CARDS_SKU, TANGEM_WALLET_3_CARDS_SKU) - fun fromSku(sku: String): ProductType? { - return when (sku) { - WALLET_2_CARDS.sku -> WALLET_2_CARDS - WALLET_3_CARDS.sku -> WALLET_3_CARDS - else -> null - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt deleted file mode 100644 index 373b8d14c8..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.tap.features.shop.domain.models - -sealed class SalesError { - data class DataError(val cause: Throwable) : SalesError() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt deleted file mode 100644 index b3b5a083d3..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.tap.features.shop.domain.models - -/** - * Sales product - * - * @property id product id - * @property productType shows TW2 cards or 3 cards - * @property state state as order available etc - * @property name product name - * @property notification optional notification - */ -data class SalesProduct( - val id: String, - val productType: ProductType, - val state: ProductState, - val name: String, - val notification: Notification?, -) - -data class Notification( - val type: String, - val title: String, - val description: String, -) - -enum class ProductState { - ORDER, - SOLD_OUT, - PRE_ORDER, -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt b/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt deleted file mode 100644 index 2e39e2bd4f..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.tap.features.shop.presentation - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase -import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase -import com.tangem.tap.features.shop.redux.ShopAction -import com.tangem.tap.proxy.AppStateHolder -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -/** - * Shop screen view model - * - * @property shopifyOrderingAvailabilityUseCase use case to define shopify ordering availability - * @property getShopifySalesProductsUseCase use case to get actual sales info - * @property dispatchers coroutine dispatchers provider - * @property appStateHolder redux state holder - * -[REDACTED_AUTHOR] - */ -@HiltViewModel -internal class ShopViewModel @Inject constructor( - private val shopifyOrderingAvailabilityUseCase: ShopifyOrderingAvailabilityUseCase, - private val getShopifySalesProductsUseCase: GetShopifySalesProductsUseCase, - private val dispatchers: CoroutineDispatcherProvider, - private val appStateHolder: AppStateHolder, -) : ViewModel() { - - /** Check ordering delay block visibility */ - fun checkOrderingDelayBlockVisibility() { - viewModelScope.launch(dispatchers.main) { - val visibility = runCatching(dispatchers.io) { shopifyOrderingAvailabilityUseCase() } - .fold(onSuccess = { !it }, onFailure = { false }) - - appStateHolder.mainStore?.dispatch(action = ShopAction.SetOrderingDelayBlockVisibility(visibility)) - } - } - - /** - * Get actual sales products info - * to configure view dynamically - */ - fun getActualSalesInfo() { - viewModelScope.launch(dispatchers.main) { - val action = getShopifySalesProductsUseCase().fold( - ifLeft = { - ShopAction.SalesProductsError - }, - ifRight = { - ShopAction.SalesProductsLoaded(it) - }, - ) - appStateHolder.mainStore?.dispatch(action) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt deleted file mode 100644 index a72324e845..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.tap.features.shop.redux - -import android.content.Intent -import com.tangem.tap.common.redux.NotificationAction -import com.tangem.tap.features.shop.domain.models.ProductType -import com.tangem.tap.common.shop.data.TangemProduct -import com.tangem.tap.common.shop.googlepay.GooglePayService -import com.tangem.tap.features.shop.domain.models.SalesProduct -import com.tangem.wallet.R -import org.rekotlin.Action - -sealed interface ShopAction : Action { - - object LoadProducts : ShopAction { - data class Success(val products: List) : ShopAction - object Failure : ShopAction, NotificationAction { - override val messageResource = R.string.common_server_unavailable - } - } - - data class ApplyPromoCode(val promoCode: String) : ShopAction { - data class Success(val promoCode: String?, val products: List) : ShopAction - object InvalidPromoCode : ShopAction - } - - object BuyWithGooglePay : ShopAction { - object UserCancelled : ShopAction - data class HandleGooglePayResponse(val resultCode: Int, val data: Intent?) : ShopAction - - data class Failure(val exception: Throwable) : ShopAction - object Success : ShopAction - } - - object StartWebCheckout : ShopAction - - data class CheckIfGooglePayAvailable(val googlePayService: GooglePayService) : ShopAction { - object Success : ShopAction - object Failure : ShopAction - } - - data class SelectProduct(val productType: ProductType) : ShopAction - - object FinishSuccessfulOrder : ShopAction - - object ResetState : ShopAction - - data class SetOrderingDelayBlockVisibility(val visibility: Boolean) : ShopAction - - data class SalesProductsLoaded(val salesProducts: List) : ShopAction - - object SalesProductsError : ShopAction -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt deleted file mode 100644 index f049ef01b6..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.tangem.tap.features.shop.redux - -import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.tap.common.analytics.events.Shop -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchOpenUrl -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.scope -import com.tangem.tap.shopService -import com.tangem.tap.store -import kotlinx.coroutines.launch -import org.rekotlin.Action -import org.rekotlin.Middleware -import timber.log.Timber - -class ShopMiddleware { - - val shopMiddleware: Middleware = { dispatch, state -> - { next -> - { action -> - handle(action) - next(action) - } - } - } -} - -@Suppress("LongMethod", "ComplexMethod") -private fun handle(action: Action) { - val shopState = store.state.shopState - - if (action is NavigationAction.NavigateTo && action.screen == AppScreen.Shop) { - store.dispatch(ShopAction.LoadProducts) - } - - if (action !is ShopAction) return - - when (action) { - is ShopAction.ApplyPromoCode -> { - scope.launch { - if (action.promoCode.isBlank() && shopState.promoCode == null) { - store.dispatchOnMain(ShopAction.ApplyPromoCode.InvalidPromoCode) - return@launch - } - - val result = shopService.applyPromoCode(action.promoCode) - - result.onSuccess { products -> - store.dispatchOnMain( - ShopAction.ApplyPromoCode.Success( - promoCode = products.first { it.type == shopState.selectedProduct }.appliedDiscount, - products = products, - ), - ) - } - result.onFailure { store.dispatchOnMain(ShopAction.ApplyPromoCode.InvalidPromoCode) } - } - } - ShopAction.BuyWithGooglePay -> { - shopService.buyWithGooglePay(shopState.selectedProduct) -// shopService.subscribeToGooglePayResult(productType = shopState.selectedProduct) { result -> -// result.onSuccess { -// store.dispatch(ShopAction.BuyWithGooglePay.Success) -// } -// result.onFailure { error -> -// if (error is TangemSdkError.UserCancelled) { -// store.dispatch(ShopAction.BuyWithGooglePay.UserCancelled) -// } else { -// store.dispatch(ShopAction.BuyWithGooglePay.Failure(error)) -// } -// } -// } - } - is ShopAction.BuyWithGooglePay.HandleGooglePayResponse -> { - scope.launch { - val result = shopService.handleGooglePayResult( - action.resultCode, - action.data, - shopState.selectedProduct, - ) - result.onSuccess { - store.dispatchOnMain(ShopAction.BuyWithGooglePay.Success) - } - result.onFailure { - store.dispatchOnMain(ShopAction.BuyWithGooglePay.Failure(it)) - } - } - } - ShopAction.LoadProducts -> { - scope.launch { - shopService.getProducts().fold( - onSuccess = { store.dispatchOnMain(ShopAction.LoadProducts.Success(it)) }, - onFailure = { - Timber.e(it) - store.dispatchOnMain(ShopAction.LoadProducts.Failure) - }, - ) - } - } - is ShopAction.CheckIfGooglePayAvailable -> { - scope.launch { - val isAvailable = - shopService.checkIfGooglePayAvailable(action.googlePayService).getOrNull() - ?: false - val newAction = if (isAvailable) { - ShopAction.CheckIfGooglePayAvailable.Success - } else { - ShopAction.CheckIfGooglePayAvailable.Failure - } - store.dispatchOnMain(newAction) - } - } - ShopAction.StartWebCheckout -> { - Analytics.send(Shop.Redirected(null)) - store.dispatchOpenUrl(shopService.getCheckoutUrl(shopState.selectedProduct)) - store.dispatch(ShopAction.FinishSuccessfulOrder) - } - - is ShopAction.FinishSuccessfulOrder -> { - scope.launch { - shopService.waitForCheckout(shopState.selectedProduct) - } - } - else -> {} - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt deleted file mode 100644 index 11061c7abf..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.tangem.tap.features.shop.redux - -import org.rekotlin.Action - -object ShopReducer { - fun reduce(action: Action, state: ShopState): ShopState = internalReduce(action, state) -} - -private fun internalReduce(action: Action, state: ShopState): ShopState { - if (action !is ShopAction) return state - - return when (action) { - is ShopAction.ApplyPromoCode -> state.copy(promoCode = action.promoCode, promoCodeLoading = true) - is ShopAction.LoadProducts.Success -> state.copy(availableProducts = action.products) - is ShopAction.ApplyPromoCode.InvalidPromoCode -> state.copy(promoCode = null, promoCodeLoading = false) - is ShopAction.ApplyPromoCode.Success -> { - state.copy( - promoCode = action.promoCode, - availableProducts = action.products, - promoCodeLoading = false, - ) - } - is ShopAction.SelectProduct -> state.copy(selectedProduct = action.productType) - - // TODO: change when we add support for GPay - is ShopAction.CheckIfGooglePayAvailable.Failure -> state.copy(isGooglePayAvailable = false) - is ShopAction.CheckIfGooglePayAvailable.Success -> state.copy(isGooglePayAvailable = false) - - is ShopAction.ResetState -> ShopState() - is ShopAction.SetOrderingDelayBlockVisibility -> state.copy(isOrderingDelayBlockVisible = action.visibility) - is ShopAction.BuyWithGooglePay, - is ShopAction.LoadProducts, - is ShopAction.StartWebCheckout, - is ShopAction.CheckIfGooglePayAvailable, - is ShopAction.BuyWithGooglePay.Failure, - is ShopAction.BuyWithGooglePay.HandleGooglePayResponse, - is ShopAction.BuyWithGooglePay.Success, - is ShopAction.BuyWithGooglePay.UserCancelled, - is ShopAction.FinishSuccessfulOrder, - is ShopAction.LoadProducts.Failure, - -> state - is ShopAction.SalesProductsLoaded -> state.copy( - salesProducts = action.salesProducts, - ) - is ShopAction.SalesProductsError -> state.copy( - salesProducts = emptyList(), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt deleted file mode 100644 index 08bac48f4d..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.tap.features.shop.redux - -import com.tangem.tap.features.shop.domain.models.ProductType -import com.tangem.tap.common.shop.data.TangemProduct -import com.tangem.tap.features.shop.domain.models.SalesProduct -import org.rekotlin.StateType - -data class ShopState( - val availableProducts: List = emptyList(), - val selectedProduct: ProductType = ProductType.WALLET_3_CARDS, - val salesProducts: List = emptyList(), - val promoCode: String? = null, - val promoCodeLoading: Boolean = false, - val isGooglePayAvailable: Boolean = false, // TODO: change when we add support for GPay - val isOrderingDelayBlockVisible: Boolean = false, -) : StateType { - val total: String? - get() = availableProducts.firstOrNull { it.type == selectedProduct }?.totalSum?.finalValue - - val priceBeforeDiscount: String? - get() { - val totalSum = availableProducts.firstOrNull { it.type == selectedProduct }?.totalSum - if (totalSum?.finalValue != totalSum?.beforeDiscount) { - return totalSum?.beforeDiscount - } - return null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt b/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt deleted file mode 100644 index eda902f707..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.tap.features.shop.toggles - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager - -internal class DefaultShopifyFeatureToggleManager( - private val featureTogglesManager: FeatureTogglesManager, -) : ShopifyFeatureToggleManager { - - override val isDynamicSalesProductsEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("SHOPIFY_DYNAMIC_ENABLED") -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt b/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt deleted file mode 100644 index ab3b8fc968..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.tap.features.shop.toggles - -/** - * Shopify feature toggle manager that provides info about shopify toggle availability - * - */ -interface ShopifyFeatureToggleManager { - - val isDynamicSalesProductsEnabled: Boolean -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt deleted file mode 100644 index bfb86a0f8a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt +++ /dev/null @@ -1,251 +0,0 @@ -package com.tangem.tap.features.shop.ui - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.content.Context -import android.os.Bundle -import android.view.View -import android.view.View.OnFocusChangeListener -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager -import androidx.activity.OnBackPressedCallback -import androidx.fragment.app.viewModels -import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.core.navigation.NavigationAction -import com.tangem.tap.common.GlobalLayoutStateHandler -import com.tangem.tap.common.KeyboardObserver -import com.tangem.tap.common.extensions.getQuantityString -import com.tangem.tap.common.extensions.show -import com.tangem.tap.features.BaseStoreFragment -import com.tangem.tap.features.shop.domain.models.ProductState -import com.tangem.tap.features.shop.domain.models.ProductType -import com.tangem.tap.features.shop.domain.models.SalesProduct -import com.tangem.tap.features.shop.presentation.ShopViewModel -import com.tangem.tap.features.shop.redux.ShopAction -import com.tangem.tap.features.shop.redux.ShopState -import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager -import com.tangem.tap.store -import com.tangem.wallet.R -import com.tangem.wallet.databinding.FragmentShopBinding -import dagger.hilt.android.AndroidEntryPoint -import org.rekotlin.StoreSubscriber -import javax.inject.Inject - -@AndroidEntryPoint -internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber { - - @Inject - lateinit var shopifyFeatureToggleManager: ShopifyFeatureToggleManager - - private val binding: FragmentShopBinding by viewBinding(FragmentShopBinding::bind) - private var cardTranslationY = 70f - - private lateinit var keyboardObserver: KeyboardObserver - - private val viewModel by viewModels() - - override fun subscribeToStore() { - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.shopState == newState.shopState - }.select { it.shopState } - } - storeSubscribersList.add(this) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) { - viewModel.getActualSalesInfo() - } else { - viewModel.checkOrderingDelayBlockVisibility() - } - - activity?.onBackPressedDispatcher?.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - store.dispatch(NavigationAction.PopBackTo()) - store.dispatch(ShopAction.ResetState) - } - }, - ) - } - - override fun onDestroyView() { - super.onDestroyView() - keyboardObserver.unregisterListener() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - setupCardsImages() - setupProductSelection() - setupPromoCodeEditText() - - binding.toolbar.setNavigationOnClickListener { - requireActivity().onBackPressed() - } - - keyboardObserver = KeyboardObserver(requireActivity()).apply { - registerListener { isVisible -> - binding.flCards.show(!isVisible) - } - } - } - - @Suppress("MagicNumber") - private fun setupCardsImages() { - GlobalLayoutStateHandler(binding.imvSecond).apply { - onStateChanged = { - cardTranslationY = it.height * 0.15f - binding.imvSecond.animate() - .translationY(cardTranslationY) - .scaleX(0.9f) - .scaleY(0.9f) - .start() - binding.imvThird.animate() - .translationY(cardTranslationY * 2) - .scaleX(0.8f) - .scaleY(0.8f) - .start() - detach() - } - } - } - - private fun setupProductSelection() = with(binding) { - chipProduct1.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_3_CARDS)) - } - chipProduct2.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_2_CARDS)) - } - chipProduct1.text = chipProduct1.getQuantityString(R.plurals.card_label_card_count, quantity = 3) - chipProduct2.text = chipProduct2.getQuantityString(R.plurals.card_label_card_count, quantity = 2) - } - - private fun setupPromoCodeEditText() = with(binding) { - etPromoCode.setOnEditorActionListener { view, actionId, event -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - val imm: InputMethodManager = - requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(view.windowToken, 0) - view.clearFocus() - return@setOnEditorActionListener true - } - return@setOnEditorActionListener false - } - - etPromoCode.onFocusChangeListener = OnFocusChangeListener { _, hasFocus -> - if (!hasFocus) { - store.dispatch(ShopAction.ApplyPromoCode(etPromoCode.text.toString())) - } - } - } - - override fun newState(state: ShopState) { - if (activity == null || view == null) return - - animateProductSelection(state.selectedProduct) - handlePriceState(state) - handlePromoCodeState(state) - // TODO: https://tangem.slack.com/archives/C01HARKDLQ0/p1691421861756069 - // if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) { - // handleNotificationBlock(state) - // } else { - // handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible) - // } - handleButtonsState(state) - } - - private fun animateProductSelection(selectedProduct: ProductType) { - val show = when (selectedProduct) { - ProductType.WALLET_2_CARDS -> false - ProductType.WALLET_3_CARDS -> true - } - showOrHideThirdCardWithAnimation(show) - } - - private fun showOrHideThirdCardWithAnimation(show: Boolean) = with(binding) { - val translationY = if (show) cardTranslationY * 2 else cardTranslationY - if (show) imvThird.show() - imvThird.animate() - .translationY(translationY) - .setListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - super.onAnimationEnd(animation) - imvThird.show(show) - } - }, - ) - } - - private fun handlePriceState(state: ShopState) = with(binding) { - tvTotal.text = state.total - tvTotalBeforeDiscount.text = state.priceBeforeDiscount - - pbPrice.show(state.total == null) - } - - private fun handlePromoCodeState(state: ShopState) = with(binding) { - if (state.promoCode == null && !etPromoCode.hasFocus()) { - etPromoCode.setText("") - } - pbPromoCode.show(state.promoCodeLoading) - } - - // private fun handleOrderingDelayBlock(isVisible: Boolean) { - // if (isVisible) binding.tvSoldOutDesc.show() else binding.tvSoldOutDesc.hide() - // } - // - // private fun handleNotificationBlock(state: ShopState) { - // if (isVisible) { - // binding.tvSoldOutDesc.show() - // getSelectedSalesProduct(state)?.notification?.let { notification -> - // binding.tvSoldOutDesc.text = notification.description - // } - // } else { - // binding.tvSoldOutDesc.hide() - // } - // } - - private fun handleButtonsState(state: ShopState) = with(binding) { - btnPayGooglePay.root.show(state.isGooglePayAvailable) - btnAlternativePayment.show(state.isGooglePayAvailable) - btnMainAction.show(!state.isGooglePayAvailable) - - if (state.total != null) { - btnAlternativePayment.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } - btnPayGooglePay.root.setOnClickListener { store.dispatch(ShopAction.BuyWithGooglePay) } - btnMainAction.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } - if (state.salesProducts.isNotEmpty()) { - getSelectedSalesProduct(state)?.let { selectedProduct -> - btnMainAction.text = getMainBtnTextByProductState( - productState = selectedProduct.state, - ) - } - } - } - } - - override fun handleOnBackPressed() { - store.dispatch(ShopAction.ResetState) - super.handleOnBackPressed() - } - - private fun getSelectedSalesProduct(state: ShopState): SalesProduct? { - return state.salesProducts.find { - it.productType == state.selectedProduct - } - } - - private fun getMainBtnTextByProductState(productState: ProductState): String = when (productState) { - ProductState.ORDER -> getString(R.string.shop_buy_now) - ProductState.SOLD_OUT -> "Sold out" // getString(R.string.sold_out) // todo finalize in next PR - ProductState.PRE_ORDER -> "Pre order" // getString(R.string.pre_order) // todo finalize in next PR - } -} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_shop.xml b/app/src/main/res/layout/fragment_shop.xml deleted file mode 100644 index 35df4922f4..0000000000 --- a/app/src/main/res/layout/fragment_shop.xml +++ /dev/null @@ -1,336 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index 580334eb2b..8ebac44c3b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -102,7 +102,6 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { ), appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey, amplitudeApiKey = configValues.amplitudeApiKey, - shopify = configValues.shopifyShop, sprinklr = configValues.sprinklr, walletConnectProjectId = configValues.walletConnectProjectId, tangemComAuthorization = configValues.tangemComAuthorization, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt index 8490f44237..25e8595d33 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt @@ -14,7 +14,6 @@ data class Config( val isTopUpEnabled: Boolean = false, @Deprecated("Not relevant since version 3.23") val isCreatingTwinCardsAllowed: Boolean = false, - val shopify: ShopifyShop? = null, val sprinklr: SprinklrConfig? = null, val walletConnectProjectId: String = "", val tangemComAuthorization: String? = null, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index eaedb32084..0e572bd3e5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -31,7 +31,6 @@ class ConfigValueModel( val blockcypherTokens: Set?, val infuraProjectId: String?, val appsFlyer: AppsFlyer, - val shopifyShop: ShopifyShop?, val sprinklr: SprinklrConfig?, val tronGridApiKey: String, val amplitudeApiKey: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/ShopifyShop.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/ShopifyShop.kt deleted file mode 100644 index c4a09adcfe..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/ShopifyShop.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.datasource.config.models - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class ShopifyShop( - @Json(name = "domain") - val domain: String, - @Json(name = "storefrontApiKeyAndroid") - val storefrontApiKey: String, - @Json(name = "merchantID") - val merchantID: String?, -) \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 1b7b643cea..ac9325b011 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -3,10 +3,6 @@ "name": "NEW_CARD_SCANNING_ENABLED", "version": "undefined" }, - { - "name": "SHOPIFY_DYNAMIC_ENABLED", - "version": "undefined" - }, { "name": "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED", "version": "undefined" diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt index 7ca534e15c..a0a6d64f25 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt @@ -11,7 +11,6 @@ data class NavigationState( enum class AppScreen(val isDialogFragment: Boolean = false) { Home, - Shop, Disclaimer, OnboardingNote, OnboardingWallet, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 02f4d4bad6..96db197034 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -61,7 +61,6 @@ okhttp = "4.9.3" rekotlin = "1.0.4" retrofit = "2.8.1" retrofitMoshiConverter = "2.6.0" -shopifyBuySdk = "12.0.0" spongycastleCryptoCore = "1.58.0.0" timber = "4.7.1" viewBindingDelegate = "1.5.9" @@ -224,7 +223,6 @@ moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okHttp-prettyLogging = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "okHttp-prettyLogging" } -shopify-buy = { module = "com.shopify.mobilebuysdk:buy3", version.ref = "shopifyBuySdk" } spongecastle-core = { module = "com.madgag.spongycastle:core", version.ref = "spongycastleCryptoCore" } reKotlin = { module = "org.rekotlin:rekotlin", version.ref = "rekotlin" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } From db178b206b7be4645865570b71deb6e39eecab4f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Mar 2024 19:24:48 +0800 Subject: [PATCH 009/109] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 3 - .../tangem/tap/common/extensions/Activity.kt | 1 + core/navigation/build.gradle.kts | 6 ++ .../core/navigation/di/EmailSenderModule.kt | 22 +++++++ .../navigation/email/AndroidEmailSender.kt | 65 +++++++++++++++++++ .../core/navigation/email/EmailSender.kt | 21 ++++++ core/utils/src/main/AndroidManifest.xml | 2 - 7 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/di/EmailSenderModule.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/email/AndroidEmailSender.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/email/EmailSender.kt delete mode 100644 core/utils/src/main/AndroidManifest.xml diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 99a374a21f..c1d5853c80 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -141,9 +141,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles - @Inject - lateinit var generalUserWalletsListManager: UserWalletsListManager - internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt b/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt index adf3e19fed..ec4f3737f0 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt @@ -17,6 +17,7 @@ import java.io.File * * */ +@Deprecated("Use EmailSender instead") fun Activity.sendEmail( email: String, subject: String, diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index bb00481f67..8dc7d5437d 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) id("configuration") } @@ -9,6 +11,10 @@ android { } dependencies { + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + implementation(deps.material) implementation(deps.reKotlin) + implementation(deps.timber) } \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/di/EmailSenderModule.kt b/core/navigation/src/main/java/com/tangem/core/navigation/di/EmailSenderModule.kt new file mode 100644 index 0000000000..0651ea8926 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/di/EmailSenderModule.kt @@ -0,0 +1,22 @@ +package com.tangem.core.navigation.di + +import android.content.Context +import com.tangem.core.navigation.email.AndroidEmailSender +import com.tangem.core.navigation.email.EmailSender +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.qualifiers.ActivityContext +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +internal object EmailSenderModule { + + @Provides + @ActivityScoped + fun provideEmailSender(@ActivityContext context: Context): EmailSender { + return AndroidEmailSender(context) + } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/email/AndroidEmailSender.kt b/core/navigation/src/main/java/com/tangem/core/navigation/email/AndroidEmailSender.kt new file mode 100644 index 0000000000..d6fe4b6d35 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/email/AndroidEmailSender.kt @@ -0,0 +1,65 @@ +package com.tangem.core.navigation.email + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.core.app.ShareCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import timber.log.Timber + +/** + * Implementation of email sender for Android + * + * @property context context + * +[REDACTED_AUTHOR] + */ +internal class AndroidEmailSender(private val context: Context) : EmailSender { + + override fun send(email: EmailSender.Email, onFail: ((Exception) -> Unit)?) { + val originalIntent = createEmailShareIntent(email) + val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")) + + val packageManager = context.packageManager + val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0) + val emailFilterIntentResults = packageManager.queryIntentActivities(emailFilterIntent, 0) + + val targetedIntents = originalIntentResults + .filter { originalResult -> + emailFilterIntentResults.any { + originalResult.activityInfo.packageName == it.activityInfo.packageName + } + } + .map { + createEmailShareIntent(email).apply { setPackage(it.activityInfo.packageName) } + } + .toMutableList() + + try { + val chooserIntent = Intent.createChooser(targetedIntents.removeAt(0), "Send mail...").apply { + putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray()) + } + + ContextCompat.startActivity(context, chooserIntent, null) + } catch (ex: Exception) { + Timber.e("Failed to send email: $ex") + } + } + + private fun createEmailShareIntent(email: EmailSender.Email): Intent { + val builder = ShareCompat.IntentBuilder(context) + .setType("message/rfc822") + .setEmailTo(arrayOf(email.address)) + .setSubject(email.subject) + .setText(email.message) + + email.attachment?.let { + builder.setStream( + FileProvider.getUriForFile(context, "${context.packageName}.provider", it), + ) + } + + return builder.intent + } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/email/EmailSender.kt b/core/navigation/src/main/java/com/tangem/core/navigation/email/EmailSender.kt new file mode 100644 index 0000000000..3aa803b702 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/email/EmailSender.kt @@ -0,0 +1,21 @@ +package com.tangem.core.navigation.email + +import java.io.File + +/** + * Email sender + * +[REDACTED_AUTHOR] + */ +interface EmailSender { + + /** Send [email] or handle exception [onFail] */ + fun send(email: Email, onFail: ((Exception) -> Unit)? = null) + + data class Email( + val address: String, + val subject: String, + val message: String, + val attachment: File? = null, + ) +} \ No newline at end of file diff --git a/core/utils/src/main/AndroidManifest.xml b/core/utils/src/main/AndroidManifest.xml deleted file mode 100644 index 02f6bf24ad..0000000000 --- a/core/utils/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file From b8c6c0471a77304b212cfad923e742720b2f5d57 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Mar 2024 12:31:42 +0300 Subject: [PATCH 010/109] Updated on 2026-08-14 --- .../scanCard/DefaultScanCardProcessor.kt | 3 - .../domain/scanCard/LegacyScanProcessor.kt | 4 -- .../domain/scanCard/UseCaseScanProcessor.kt | 40 +++++++++---- .../domain/scanCard/chains/AnalyticsChain.kt | 8 +-- .../chains/CheckForOnboardingChain.kt | 8 +-- .../domain/scanCard/chains/DisclaimerChain.kt | 13 ++--- .../chains/FailedScansCounterChain.kt | 29 ++++++++++ .../scanCard/chains/ScanChainException.kt | 6 +- .../domain/scanCard/chains/ScanChainResult.kt | 7 +++ .../scanCard/chains/ScanningFinishedChain.kt | 25 -------- .../repository/DefaultScanCardRepository.kt | 13 ++--- .../tap/features/home/redux/HomeAction.kt | 3 - .../tap/features/home/redux/HomeMiddleware.kt | 25 ++++---- .../tap/features/home/redux/HomeReducer.kt | 3 - .../tap/features/home/redux/HomeState.kt | 5 +- .../tangem/domain/card/ScanCardException.kt | 14 +++-- .../tangem/domain/card/ScanCardProcessor.kt | 1 - .../com/tangem/domain/card/ScanCardUseCase.kt | 57 +++++++++---------- .../card/repository/ScanCardRepository.kt | 9 +-- .../com/tangem/domain/core/chain/Chain.kt | 18 ------ .../domain/core/chain/ChainProcessor.kt | 21 +++---- .../com/tangem/domain/core/chain/Chains.kt | 44 ++++++++++++++ 22 files changed, 191 insertions(+), 165 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/scanCard/chains/FailedScansCounterChain.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainResult.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt delete mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chains.kt diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt index 1bac186d83..06df7c2274 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt @@ -30,7 +30,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor { analyticsEvent: AnalyticsEvent?, cardId: String?, onProgressStateChange: suspend (showProgress: Boolean) -> Unit, - onScanStateChange: suspend (scanInProgress: Boolean) -> Unit, onWalletNotCreated: suspend () -> Unit, disclaimerWillShow: () -> Unit, onFailure: suspend (error: TangemError) -> Unit, @@ -41,7 +40,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor { analyticsEvent, cardId, onProgressStateChange, - onScanStateChange, onWalletNotCreated, disclaimerWillShow, onFailure, @@ -52,7 +50,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor { analyticsEvent, cardId, onProgressStateChange, - onScanStateChange, onWalletNotCreated, disclaimerWillShow, onFailure, diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index 757e78cb34..cf1c9f95be 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -48,14 +48,12 @@ internal object LegacyScanProcessor { analyticsEvent: AnalyticsEvent?, cardId: String?, onProgressStateChange: suspend (showProgress: Boolean) -> Unit, - onScanStateChange: suspend (scanInProgress: Boolean) -> Unit, onWalletNotCreated: suspend () -> Unit, disclaimerWillShow: () -> Unit, onFailure: suspend (error: TangemError) -> Unit, onSuccess: suspend (scanResponse: ScanResponse) -> Unit, ) = withMainContext { onProgressStateChange(true) - onScanStateChange(true) tangemSdkManager.changeDisplayedCardIdNumbersCount(null) @@ -65,13 +63,11 @@ internal object LegacyScanProcessor { result .doOnFailure { error -> - onScanStateChange(false) onFailure(error) } .doOnSuccess { scanResponse -> tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) - onScanStateChange(false) sendAnalytics(analyticsEvent, scanResponse) showDisclaimerIfNeed( diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index 02e1701894..3a0902520a 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -3,11 +3,15 @@ package com.tangem.tap.domain.scanCard import arrow.fx.coroutines.resourceScope import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError +import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.Basic import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.StateDialog import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse +import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.scanCard.chains.* @@ -25,9 +29,15 @@ internal object UseCaseScanProcessor { allowsRequestAccessCodeFromRepository: Boolean = false, ): CompletionResult { val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase) + return scanCardUseCase(cardId, allowsRequestAccessCodeFromRepository) .fold( - ifLeft = { CompletionResult.Failure(scanCardExceptionConverter.convertBack(it)) }, + ifLeft = { + val error = scanCardExceptionConverter.convertBack(it) + + Analytics.send(Basic.ScanError(error)) + CompletionResult.Failure(error) + }, ifRight = { CompletionResult.Success(it) }, ) } @@ -37,17 +47,14 @@ internal object UseCaseScanProcessor { analyticsEvent: AnalyticsEvent?, cardId: String?, onProgressStateChange: suspend (showProgress: Boolean) -> Unit, - onScanStateChange: suspend (scanInProgress: Boolean) -> Unit, onWalletNotCreated: suspend () -> Unit, disclaimerWillShow: () -> Unit, onFailure: suspend (error: TangemError) -> Unit, onSuccess: suspend (scanResponse: ScanResponse) -> Unit, ) = progressScope(onProgressStateChange) { - onScanStateChange(true) - val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase) val chains = buildList { - add(ScanningFinishedChain { onScanStateChange(false) }) + add(FailedScansCounterChain(UseCaseScanProcessor::showMaxUnsuccessfulScansReachedDialog)) if (analyticsEvent != null) { add(AnalyticsChain(analyticsEvent)) } @@ -55,9 +62,14 @@ internal object UseCaseScanProcessor { add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager)) } - scanCardUseCase(cardId, afterScanChains = chains) - .map { onSuccess(it) } - .mapLeft { proceedWithException(it, onWalletNotCreated, onFailure) } + scanCardUseCase(cardId, afterScanChains = chains).fold( + ifLeft = { proceedWithException(it, onWalletNotCreated, onFailure) }, + ifRight = { onSuccess(it) }, + ) + } + + private fun showMaxUnsuccessfulScansReachedDialog() { + store.dispatchDialogShow(StateDialog.ScanFailsDialog) } private suspend fun proceedWithException( @@ -75,7 +87,12 @@ internal object UseCaseScanProcessor { is ScanCardException.UserCancelled, is ScanCardException.WrongAccessCode, is ScanCardException.WrongCardId, - -> onFailure(scanCardExceptionConverter.convertBack(exception)) + -> { + val error = scanCardExceptionConverter.convertBack(exception) + + Analytics.send(Basic.ScanError(error)) + onFailure(error) + } } } @@ -89,8 +106,9 @@ internal object UseCaseScanProcessor { navigateTo(exception.onboardingRoute) onWalletNotCreated() } - is ScanChainException.DisclaimerWasCanceled, - -> onFailure(scanCardExceptionConverter.convertBack(exception)) + is ScanChainException.DisclaimerWasCanceled -> { + onFailure(scanCardExceptionConverter.convertBack(exception)) + } } } diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt index 8f72ce92f8..c7c56864ec 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt @@ -1,11 +1,11 @@ package com.tangem.tap.domain.scanCard.chains -import arrow.core.Either import arrow.core.right import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.card.ScanCardException import com.tangem.domain.core.chain.Chain +import com.tangem.domain.core.chain.ResultChain import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor @@ -19,11 +19,9 @@ import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor */ class AnalyticsChain( private val event: AnalyticsEvent, -) : Chain { +) : ResultChain() { - override suspend fun invoke( - previousChainResult: ScanResponse, - ): Either { + override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult { val interceptor = CardContextInterceptor(previousChainResult) val params = event.params.toMutableMap() interceptor.intercept(params) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt index 3db1f7a79e..66f708e28d 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -1,6 +1,5 @@ package com.tangem.tap.domain.scanCard.chains -import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.core.analytics.Analytics @@ -9,6 +8,7 @@ import com.tangem.domain.card.ScanCardException import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.core.chain.Chain +import com.tangem.domain.core.chain.ResultChain import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.addContext import com.tangem.tap.common.extensions.dispatchOnMain @@ -39,11 +39,9 @@ import org.rekotlin.Store class CheckForOnboardingChain( private val store: Store, private val tapWalletManager: TapWalletManager, -) : Chain { +) : ResultChain() { - override suspend fun invoke( - previousChainResult: ScanResponse, - ): Either { + override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult { tapWalletManager.updateConfigManager(previousChainResult) store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult)) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt index ccf4f495bd..35ad2efbab 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt @@ -1,11 +1,11 @@ package com.tangem.tap.domain.scanCard.chains -import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.core.navigation.AppScreen import com.tangem.domain.card.ScanCardException import com.tangem.domain.core.chain.Chain +import com.tangem.domain.core.chain.ResultChain import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState @@ -31,11 +31,9 @@ import kotlin.coroutines.resume internal class DisclaimerChain( private val store: Store, private val disclaimerWillShow: () -> Unit = {}, -) : Chain { +) : ResultChain() { - override suspend fun invoke( - previousChainResult: ScanResponse, - ): Either { + override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult { val disclaimer = previousChainResult.card.createDisclaimer() return if (disclaimer.isAccepted()) { @@ -46,10 +44,7 @@ internal class DisclaimerChain( } } - private suspend fun showDisclaimer( - disclaimer: Disclaimer, - response: ScanResponse, - ): Either { + private suspend fun showDisclaimer(disclaimer: Disclaimer, response: ScanResponse): ScanChainResult { store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer)) return suspendCancellableCoroutine { continuation -> diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/FailedScansCounterChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/FailedScansCounterChain.kt new file mode 100644 index 0000000000..9698824c24 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/FailedScansCounterChain.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.domain.scanCard.chains + +import com.tangem.domain.card.ScanCardException +import com.tangem.domain.core.chain.Chain +import com.tangem.domain.models.scan.ScanResponse + +internal class FailedScansCounterChain( + private val onMaxUnsuccessfulScansReached: () -> Unit, + private val maxUnsuccessfulScans: Int = 3, +) : Chain { + + override suspend fun launch(previousChainResult: ScanChainResult): ScanChainResult { + if (previousChainResult.isLeft()) { + unsuccessfulScansCounter = unsuccessfulScansCounter.inc().coerceAtMost(maxUnsuccessfulScans) + if (unsuccessfulScansCounter == maxUnsuccessfulScans) { + onMaxUnsuccessfulScansReached() + } + } else { + unsuccessfulScansCounter = 0 + } + + return previousChainResult + } + + private companion object { + + var unsuccessfulScansCounter = 0 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt index 588dc809e2..ae5e9f8ad3 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt @@ -8,7 +8,11 @@ sealed class ScanChainException : ScanCardException.ChainException() { /** * May be returned from [DisclaimerChain] * */ - object DisclaimerWasCanceled : ScanChainException() + data object DisclaimerWasCanceled : ScanChainException() { + + @Suppress("UnusedPrivateMember") + private fun readResolve(): Any = DisclaimerWasCanceled + } /** * May be returned from [CheckForOnboardingChain] diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainResult.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainResult.kt new file mode 100644 index 0000000000..e83f4a1578 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainResult.kt @@ -0,0 +1,7 @@ +package com.tangem.tap.domain.scanCard.chains + +import arrow.core.Either +import com.tangem.domain.card.ScanCardException +import com.tangem.domain.models.scan.ScanResponse + +internal typealias ScanChainResult = Either \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt deleted file mode 100644 index a8c116cbe8..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.tap.domain.scanCard.chains - -import arrow.core.Either -import arrow.core.right -import com.tangem.domain.card.ScanCardException -import com.tangem.domain.core.chain.Chain -import com.tangem.domain.models.scan.ScanResponse - -/** - * Responsible for invoking the callback at the end of the card scanning operation. Should be passed as last chain in - * after card scanning chains. Always returns result of previous chain. - * - * @param onScanningFinished a suspending function to be called when the scanning process has finished. - * - * @see Chain for more information about the Chain interface. - */ -internal class ScanningFinishedChain( - private val onScanningFinished: suspend () -> Unit, -) : Chain { - - override suspend fun invoke(previousChainResult: ScanResponse): Either { - onScanningFinished() - return previousChainResult.right() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt index f74d706775..8aeacbb2c9 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt @@ -1,32 +1,27 @@ package com.tangem.tap.domain.scanCard.repository -import arrow.core.Either -import arrow.core.raise.either import com.tangem.common.CompletionResult -import com.tangem.domain.card.ScanCardException import com.tangem.domain.card.repository.ScanCardRepository import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter +// TODO: Move to the :data:card module internal class DefaultScanCardRepository( private val tangemSdkManager: TangemSdkManager, ) : ScanCardRepository { private val exceptionConverter = ScanCardExceptionConverter() - override suspend fun scanCard( - cardId: String?, - allowRequestAccessCodeFromStorage: Boolean, - ): Either = either { - when ( + override suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse { + return when ( val result = tangemSdkManager.scanProduct( cardId = cardId, allowsRequestAccessCodeFromRepository = allowRequestAccessCodeFromStorage, ) ) { is CompletionResult.Success -> result.data - is CompletionResult.Failure -> raise(exceptionConverter.convert(result.error)) + is CompletionResult.Failure -> throw exceptionConverter.convert(result.error) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index e87ba1ee99..cb2d157825 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -3,7 +3,6 @@ package com.tangem.tap.features.home.redux import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.tap.common.entities.IndeterminateProgressButton import kotlinx.coroutines.CoroutineScope import org.rekotlin.Action @@ -29,6 +28,4 @@ sealed class HomeAction : Action { data class GoToShop(val userCountryCode: String?) : HomeAction() data class UpdateCountryCode(val userCountryCode: String) : HomeAction() - - data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 2bec7b7af6..8b83c455cf 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -13,12 +13,10 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.analytics.events.Shop -import com.tangem.tap.common.entities.IndeterminateProgressButton import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL -import com.tangem.tap.features.send.redux.states.ButtonState import com.tangem.tap.features.signin.redux.SignInAction import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.redux.DaggerGraphState @@ -31,6 +29,8 @@ import org.rekotlin.Action import org.rekotlin.Middleware import timber.log.Timber +private const val HIDE_PROGRESS_DELAY = 400L + object HomeMiddleware { val handler = homeMiddleware @@ -84,17 +84,16 @@ private suspend fun readCard(analyticsEvent: AnalyticsEvent?) { analyticsEvent = analyticsEvent, onProgressStateChange = { showProgress -> if (showProgress) { - changeButtonState(ButtonState.PROGRESS) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = true)) } else { - changeButtonState(ButtonState.ENABLED) + delay(HIDE_PROGRESS_DELAY) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) } }, - onScanStateChange = { scanInProgress -> - store.dispatch(HomeAction.ScanInProgress(scanInProgress)) - }, onFailure = { Timber.e(it, "Unable to scan card") - changeButtonState(ButtonState.ENABLED) + delay(HIDE_PROGRESS_DELAY) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) }, onSuccess = { scanResponse -> proceedWithScanResponse(scanResponse) @@ -113,7 +112,7 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch { Timber.e(error, "Unable to save user wallet") } .doOnSuccess { - scope.launch { store.onUserWalletSelected(userWallet = userWallet) } + scope.launch { store.onUserWalletSelected(userWallet) } } .doOnResult { store.dispatchOnMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Card)) @@ -123,10 +122,6 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch { private suspend fun navigateTo(appScreen: AppScreen) { store.dispatchOnMain(NavigationAction.NavigateTo(appScreen)) - delay(timeMillis = 200) - changeButtonState(ButtonState.ENABLED) -} - -private fun changeButtonState(state: ButtonState) { - store.dispatchOnMain(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state))) + delay(HIDE_PROGRESS_DELAY) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt index 81f1adee30..bc71e1fef2 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt @@ -22,9 +22,6 @@ private fun internalReduce(action: Action, appState: AppState): HomeState { is HomeAction.ScanInProgress -> { state = state.copy(scanInProgress = action.scanInProgress) } - is HomeAction.ChangeScanCardButtonState -> { - state = state.copy(btnScanState = action.state) - } is HomeAction.UpdateCountryCode -> { state.onCountryCodeUpdate(state, action.userCountryCode) } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt index 7ffd5d139e..2e584d3d35 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt @@ -1,15 +1,14 @@ package com.tangem.tap.features.home.redux +import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import com.tangem.tap.common.entities.IndeterminateProgressButton -import com.tangem.tap.features.send.redux.states.ButtonState import org.rekotlin.StateType import java.util.Locale +@Immutable data class HomeState( val scanInProgress: Boolean = false, - val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED), val stories: List = initDefaultStories(), ) : StateType { diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt index 3f153ee689..667e29f062 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt @@ -1,14 +1,20 @@ package com.tangem.domain.card -// TODO: May be add new error types sealed class ScanCardException : Exception() { - object UserCancelled : ScanCardException() - object WrongAccessCode : ScanCardException() + data object UserCancelled : ScanCardException() { + @Suppress("UnusedPrivateMember") + private fun readResolve(): Any = UserCancelled + } + + data object WrongAccessCode : ScanCardException() { + @Suppress("UnusedPrivateMember") + private fun readResolve(): Any = WrongAccessCode + } open class ChainException : ScanCardException() - data class UnknownException(override val cause: Exception) : ScanCardException() + data class UnknownException(override val cause: Throwable) : ScanCardException() data class WrongCardId(val cardId: String) : ScanCardException() } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt index 961d9acd07..4dfc1c9668 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt @@ -16,7 +16,6 @@ interface ScanCardProcessor { analyticsEvent: AnalyticsEvent? = null, cardId: String? = null, onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {}, - onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {}, onWalletNotCreated: suspend () -> Unit = {}, disclaimerWillShow: () -> Unit = {}, onFailure: suspend (error: TangemError) -> Unit = {}, diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt index 69dbc7a617..04abfb1fce 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt @@ -1,9 +1,7 @@ package com.tangem.domain.card import arrow.core.Either -import arrow.core.EitherNel -import arrow.core.flatMap -import com.tangem.TangemSdk +import arrow.core.raise.either import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.ScanCardRepository import com.tangem.domain.core.chain.Chain @@ -11,12 +9,7 @@ import com.tangem.domain.core.chain.ChainProcessor import com.tangem.domain.models.scan.ScanResponse /** - * Use case responsible for scanning a card and returning a [ScanResponse] object - * - * @property scanCardRepository a repository object implementing [ScanCardRepository] interface - * @property cardSdkConfigRepository an instance of [TangemSdk] to configure the display format of the card ID - * - * @constructor create a new instance of [ScanCardUseCase] with the given dependencies + * Use case responsible for scanning a card and returning a [ScanResponse] object. */ class ScanCardUseCase( private val scanCardRepository: ScanCardRepository, @@ -24,37 +17,41 @@ class ScanCardUseCase( ) { /** A [ChainProcessor] object to launch the after-scan chains */ - private val scanChainProcessor by lazy { - ChainProcessor() - } + private val scanChainProcessor = ChainProcessor() /** - * Scan a card and return a [ScanResponse] object. + * Scan a card. * - * @param cardId an optional card ID to scan. If null, can scan any card present - * @param allowRequestAccessCodeFromStorage whether to prompt the user for an access code if needed - * @param afterScanChains a list of chains that should be executed after a successful card scan - * operation. Defaults to an empty array + * @param cardId An optional card ID to scan. If `null`, can scan any card present + * @param allowRequestAccessCodeFromStorage Whether to prompt the user for an access code if needed + * @param afterScanChains A list of chains that should be executed after a card scan + * operation. Defaults to an empty array. * - * @return a [EitherNel] object with either a non-empty list of [ScanCardException] or a [ScanResponse] + * @return [Either] object with either a [ScanCardException] or a [ScanResponse] object. */ suspend operator fun invoke( cardId: String? = null, allowRequestAccessCodeFromStorage: Boolean = false, - afterScanChains: List> = emptyList(), - ): Either { + afterScanChains: List> = emptyList(), + ): Either = either { cardSdkConfigRepository.resetCardIdDisplayFormat() - scanChainProcessor.addChains(afterScanChains) + scanChainProcessor.setChains(afterScanChains) - return scanCardRepository.scanCard( - cardId = cardId, - allowRequestAccessCodeFromStorage = allowRequestAccessCodeFromStorage, - ) - .onRight { scanResponse -> - cardSdkConfigRepository.updateCardIdDisplayFormat(scanResponse.productType) - } - .flatMap { response -> - scanChainProcessor.launchChains(initial = response) + val maybeScanResponse = scanCard(cardId, allowRequestAccessCodeFromStorage) + val scanResponse = scanChainProcessor.launchChains(maybeScanResponse).bind() + + cardSdkConfigRepository.updateCardIdDisplayFormat(scanResponse.productType) + + scanResponse + } + + private suspend fun scanCard( + cardId: String?, + allowRequestAccessCodeFromStorage: Boolean, + ): Either { + return Either.catch { scanCardRepository.scanCard(cardId, allowRequestAccessCodeFromStorage) } + .mapLeft { e -> + e as? ScanCardException ?: ScanCardException.UnknownException(e) } } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt index 2de00090cf..90f2016383 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt @@ -1,6 +1,5 @@ package com.tangem.domain.card.repository -import arrow.core.Either import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse @@ -15,10 +14,8 @@ interface ScanCardRepository { * @param cardId an optional card ID to scan. If null, the repository should scan any card present. * @param allowRequestAccessCodeFromStorage whether the access code can be requested from * internal storage during the scan process - * @return an [Either] that contains a [ScanCardException] in case of an error or a [ScanResponse]. + * @return a [ScanResponse] object with the result of the scan. + * @throws [ScanCardException] if the scan process fails. */ - suspend fun scanCard( - cardId: String?, - allowRequestAccessCodeFromStorage: Boolean, - ): Either + suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt deleted file mode 100644 index a7e0d5bc38..0000000000 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.domain.core.chain - -import arrow.core.Either - -/** - * A chain in the [ChainProcessor] class for processing a chain of operations with the ability to handle errors. - * @param E the type of error - * @param R the type of result - */ -interface Chain { - - /** - * Invokes the chain with the previous chain result as input and returns an [Either] result. - * @param previousChainResult the previous chain result as an [R] - * @return the result of the chain processing as an [Either] - */ - suspend operator fun invoke(previousChainResult: R): Either -} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt index c1cac09eb3..4e95318b6c 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt @@ -1,7 +1,6 @@ package com.tangem.domain.core.chain import arrow.core.Either -import arrow.core.raise.either // TODO: Create Ior implementation // TODO: Create accumulate implementation @@ -10,7 +9,7 @@ import arrow.core.raise.either * @param E the type of error * @param R the type of result */ -class ChainProcessor { +class ChainProcessor { /** * The list of chains to be executed in order. @@ -18,18 +17,20 @@ class ChainProcessor { private val chains: MutableList> = mutableListOf() /** - * Adds chains to the existing list of chains to be executed. - * @param chains the chains to be added to the list + * Sets the list of chains to be executed. + * @param chains the chains to be set */ - fun addChains(chains: List>) { + fun setChains(chains: List>) { + this.chains.clear() this.chains.addAll(chains) } - suspend fun launchChains(initial: R): Either { - return either { - chains.fold(initial) { prevChainResult, chain -> - chain.invoke(prevChainResult).bind() - } + /** + * Launches the chains with the [initial] value. + * */ + suspend fun launchChains(initial: Either): Either { + return chains.fold(initial) { prevChainResult, chain -> + chain.launch(prevChainResult) } } } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chains.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chains.kt new file mode 100644 index 0000000000..02b5da3924 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chains.kt @@ -0,0 +1,44 @@ +package com.tangem.domain.core.chain + +import arrow.core.Either +import arrow.core.flatMap + +/** + * A chain in the [ChainProcessor] class for processing a chain of operations with the ability to handle errors. + * @param E the type of error + * @param R the type of result + */ +interface Chain { + + /** + * Invokes the chain with the previous chain result as input and returns an [Either] result. + * + * @param previousChainResult the previous chain result as an [Either] + * @return the result of the chain processing as an [Either] + */ + suspend fun launch(previousChainResult: Either): Either +} + +/** + * A chain in the [ChainProcessor] class for processing a chain of operations with the ability to handle errors. + * + * This chain will be launched only if the previous chains in the [ChainProcessor] have + * returned a right side of [Either]. + * + * @param E the type of error + * @param R the type of result + */ +abstract class ResultChain : Chain { + + final override suspend fun launch(previousChainResult: Either): Either { + return previousChainResult.flatMap { launch(it) } + } + + /** + * Invokes the chain with the previous chain result as input and returns a result. + * + * @param previousChainResult the previous chain result + * @return the result of the chain processing as an [Either] + */ + abstract suspend fun launch(previousChainResult: R): Either +} \ No newline at end of file From 24c6425299fdb319084384785abccef4c76c5b0f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Mar 2024 20:30:58 +0100 Subject: [PATCH 011/109] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 12 +- .../viewmodels/AddCustomTokenViewModel.kt | 3 +- .../DefaultNetworksCompatibilityRepository.kt | 11 ++ .../RequiresHardenedDerivationOnlyUseCase.kt | 26 ++++ .../NetworksCompatibilityRepository.kt | 2 + .../factory/AddCustomTokenStateFactory.kt | 125 +++++++++++++----- .../viewmodels/AddCustomTokenViewModel.kt | 45 ++++--- 7 files changed, 170 insertions(+), 54 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index c67960a860..7c830de869 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -278,9 +278,17 @@ internal object TokensDomainModule { @Provides @ViewModelScoped fun provideCheckTokenCompatibilityUseCase( - repository: NetworksCompatibilityRepository, + networksCompatibilityRepository: NetworksCompatibilityRepository, ): CheckCurrencyCompatibilityUseCase { - return CheckCurrencyCompatibilityUseCase(repository) + return CheckCurrencyCompatibilityUseCase(networksCompatibilityRepository) + } + + @Provides + @ViewModelScoped + fun provideNeedHardenedDerivationUseCase( + networksCompatibilityRepository: NetworksCompatibilityRepository, + ): RequiresHardenedDerivationOnlyUseCase { + return RequiresHardenedDerivationOnlyUseCase(networksCompatibilityRepository) } @Provides diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index b15a5fdee5..4efb2848fc 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -15,7 +15,6 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.crypto.hdWallet.HDWalletError import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.* import com.tangem.domain.common.util.cardTypesResolver @@ -674,7 +673,7 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun createDerivationPathOrNull(rawPath: String): DerivationPath? { return try { DerivationPath(rawPath) - } catch (error: HDWalletError) { + } catch (error: Throwable) { null } } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt index f17fe671b9..006ca60bb9 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt @@ -1,8 +1,10 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve import com.tangem.data.tokens.utils.getNetwork import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.extensions.* import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider @@ -71,6 +73,15 @@ internal class DefaultNetworksCompatibilityRepository( } } + override suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean { + val scanResponse = getWalletOrThrow(userWalletId).scanResponse + val config = CardConfig.createConfig(scanResponse.card) + val blockchain = Blockchain.fromNetworkId(networkId) ?: return false + + return config.primaryCurve(blockchain) == EllipticCurve.Ed25519Slip0010 && + scanResponse.cardTypesResolver.isWallet2() + } + override fun areTokensSupportedByNetwork(networkId: String): Boolean { return Blockchain.fromNetworkId(networkId)?.canHandleTokens() ?: false } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt new file mode 100644 index 0000000000..db8dad152a --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.tokens.repository.NetworksCompatibilityRepository +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Ed25519Slip0010 requires only hardened derivations on Wallet 2 + */ +class RequiresHardenedDerivationOnlyUseCase( + private val repository: NetworksCompatibilityRepository, +) { + + suspend operator fun invoke(networkId: String, userWalletId: UserWalletId): Either { + return either { + catch( + block = { + repository.requiresHardenedDerivationOnly(networkId, userWalletId) + }, + catch = { throwable -> raise(throwable) }, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt index 1d0f6946b1..a5e2083e05 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt @@ -17,5 +17,7 @@ interface NetworksCompatibilityRepository { @Throws(IllegalArgumentException::class) suspend fun getSupportedNetworks(userWalletId: UserWalletId): List + suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean + fun areTokensSupportedByNetwork(networkId: String): Boolean } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt index 7fe3b1dc8c..6ad1ac8e68 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt @@ -2,6 +2,7 @@ package com.tangem.managetokens.presentation.addcustomtoken.state.factory import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent +import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.tokens.error.AddCustomTokenError import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet @@ -15,6 +16,7 @@ import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentSet +@Suppress("LargeClass") internal class AddCustomTokenStateFactory( private val currentStateProvider: Provider, private val clickIntents: AddCustomTokenClickIntents, @@ -72,8 +74,11 @@ internal class AddCustomTokenStateFactory( ) } - private fun getListOfDerivations(supportedNetworks: List): List { - return supportedNetworks.mapNotNull { network -> + private fun getListOfDerivations( + networksListToGenerateDerivations: List, + filterOnlyHardenedDerivations: Boolean = false, + ): List { + return networksListToGenerateDerivations.mapNotNull { network -> network.derivationPath.value?.let { rawPath -> Derivation( networkName = network.name, @@ -82,6 +87,12 @@ internal class AddCustomTokenStateFactory( networkId = network.backendId, onDerivationSelected = clickIntents::onDerivationSelected, ) + }.takeIf { derivation -> + if (filterOnlyHardenedDerivations) { + derivation?.let { allNodesHardened(createDerivationPathOrNull(it.path)) } ?: false + } else { + true + } } } } @@ -218,41 +229,52 @@ internal class AddCustomTokenStateFactory( ) } - fun updateStateOnNetworkSelected(networkItemState: NetworkItemState, supportsTokens: Boolean): AddCustomTokenState { + fun updateStateOnNetworkSelected( + networkItemState: NetworkItemState, + supportsTokens: Boolean, + networks: List, + requiresHardenedDerivationOnly: Boolean, + ): AddCustomTokenState { val uiState = currentStateProvider() val tokenData = if (supportsTokens) { - uiState.tokenData - ?: CustomTokenData( - contractAddressTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onContractAddressChange, - onFocusExit = clickIntents::onContractAddressFocusExit, - ), - nameTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onTokenNameChange, - onFocusExit = clickIntents::onTokenNameFocusExit, - ), - symbolTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onSymbolChange, - onFocusExit = clickIntents::onSymbolFocusExit, - ), - decimalsTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onDecimalsChange, - onFocusExit = clickIntents::onDecimalsFocusExit, - ), - ) + uiState.tokenData ?: CustomTokenData( + contractAddressTextField = TextFieldState.Editable( + value = "", + isEnabled = true, + onValueChange = clickIntents::onContractAddressChange, + onFocusExit = clickIntents::onContractAddressFocusExit, + ), + nameTextField = TextFieldState.Editable( + value = "", + isEnabled = false, + onValueChange = clickIntents::onTokenNameChange, + onFocusExit = clickIntents::onTokenNameFocusExit, + ), + symbolTextField = TextFieldState.Editable( + value = "", + isEnabled = false, + onValueChange = clickIntents::onSymbolChange, + onFocusExit = clickIntents::onSymbolFocusExit, + ), + decimalsTextField = TextFieldState.Editable( + value = "", + isEnabled = false, + onValueChange = clickIntents::onDecimalsChange, + onFocusExit = clickIntents::onDecimalsFocusExit, + ), + ) } else { null } + + val derivations = getListOfDerivations(networks, requiresHardenedDerivationOnly) + val chooseDerivationState = createChooseDerivationState(derivations) + return uiState.copy( - chooseNetworkState = uiState.chooseNetworkState.copy(selectedNetwork = networkItemState), + chooseNetworkState = uiState.chooseNetworkState.copy( + selectedNetwork = networkItemState, + ), + chooseDerivationState = chooseDerivationState, tokenData = tokenData, addTokenButton = uiState.addTokenButton.copy(isEnabled = true), ) @@ -291,6 +313,47 @@ internal class AddCustomTokenStateFactory( ) } + fun updateOnCustomDerivationEntered(input: String, requiresHardenedDerivationOnly: Boolean): AddCustomTokenState { + val uiState = currentStateProvider() + val path = createDerivationPathOrNull(input) + val isWrongDerivationForWallet2 = isWrongDerivationForWallet2( + requiresHardenedDerivationOnly = requiresHardenedDerivationOnly, + derivationPath = path, + ) + val enterDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy( + confirmButtonEnabled = path != null && !isWrongDerivationForWallet2, + derivationIncorrect = input.isNotBlank() && path == null || isWrongDerivationForWallet2, + ) + return uiState.copy( + chooseDerivationState = uiState.chooseDerivationState?.copy( + enterCustomDerivationState = enterDerivationState, + ), + ) + } + + private fun isWrongDerivationForWallet2( + requiresHardenedDerivationOnly: Boolean, + derivationPath: DerivationPath?, + ): Boolean { + return if (requiresHardenedDerivationOnly) { + !allNodesHardened(derivationPath) + } else { + false + } + } + + private fun allNodesHardened(derivationPath: DerivationPath?): Boolean { + return derivationPath?.nodes?.all { it.isHardened } ?: false + } + + private fun createDerivationPathOrNull(rawPath: String): DerivationPath? { + return try { + DerivationPath(rawPath) + } catch (error: Throwable) { + null + } + } + fun updateStateOnLoadingTokenInfo(contractAddress: String): AddCustomTokenState { return currentStateProvider().copy( tokenData = CustomTokenData( diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt index 1b107076ea..44e55b947d 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt @@ -9,8 +9,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.crypto.hdWallet.HDWalletError import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency @@ -56,6 +54,7 @@ internal class AddCustomTokenViewModel @Inject constructor( private val validateContractAddressUseCase: ValidateContractAddressUseCase, private val getNetworksSupportedByWallet: GetNetworksSupportedByWallet, private val areTokensSupportedByNetworkUseCase: AreTokensSupportedByNetworkUseCase, + private val requiresHardenedDerivationOnlyUseCase: RequiresHardenedDerivationOnlyUseCase, private val analyticsEventHandler: AnalyticsEventHandler, ) : ViewModel(), AddCustomTokenClickIntents, DefaultLifecycleObserver { @@ -135,13 +134,25 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun selectNetwork(networkItemState: NetworkItemState) { viewModelScope.launch(dispatchers.io) { - val selectedWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId + // TODO [REDACTED_TASK_KEY] + val selectedWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId ?: return@launch val supportsTokens = areTokensSupportedByNetworkUseCase( networkId = networkItemState.id, userWalletId = selectedWalletId, ).getOrNull() ?: false + + val networksForDerivations = getSupportedNetworks(selectedWalletId) + withContext(dispatchers.main) { - uiState = stateFactory.updateStateOnNetworkSelected(networkItemState, supportsTokens) + uiState = stateFactory.updateStateOnNetworkSelected( + networkItemState = networkItemState, + supportsTokens = supportsTokens, + networks = networksForDerivations, + requiresHardenedDerivationOnly = requiresHardenedDerivationOnly( + networkId = networkItemState.id, + userWalletId = selectedWalletId, + ), + ) } } } @@ -347,6 +358,8 @@ internal class AddCustomTokenViewModel @Inject constructor( } override fun onCustomDerivationChange(input: String) { + val selectedWallet = getSelectedWalletSyncUseCase().getOrNull() ?: return + analyticsEventHandler.send(ManageTokens.CustomTokenDerivationSelected(ManageTokens.Derivation.CUSTOM.value)) uiState = uiState.copy( chooseDerivationState = uiState.chooseDerivationState?.copy( @@ -356,25 +369,19 @@ internal class AddCustomTokenViewModel @Inject constructor( ), ) debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) { - val path = createDerivationPathOrNull(input) - val enterDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy( - confirmButtonEnabled = path != null, - derivationIncorrect = input.isNotBlank() && path == null, - ) - uiState = uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy( - enterCustomDerivationState = enterDerivationState, - ), + val networkId = uiState.chooseNetworkState.selectedNetwork?.id ?: return@debounce + uiState = stateFactory.updateOnCustomDerivationEntered( + input = input, + requiresHardenedDerivationOnly = requiresHardenedDerivationOnly(networkId, selectedWallet.walletId), ) } } - private fun createDerivationPathOrNull(rawPath: String): DerivationPath? { - return try { - DerivationPath(rawPath) - } catch (error: HDWalletError) { - null - } + private suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean { + return requiresHardenedDerivationOnlyUseCase.invoke( + networkId = networkId, + userWalletId = userWalletId, + ).getOrElse { false } } override fun onCustomDerivationSelected() { From 21a63d246b85b81ce14e3bd2028bfd74d4a58481 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Mar 2024 17:52:11 +0800 Subject: [PATCH 012/109] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tap/common/feedback/FeedbackData.kt | 10 +-- domain/feedback/.gitignore | 1 + domain/feedback/build.gradle.kts | 13 ++++ .../domain/feedback/FeedbackDataBuilder.kt | 78 +++++++++++++++++++ .../domain/feedback/models/BlockchainInfo.kt | 18 +++++ .../tangem/domain/feedback/models/CardInfo.kt | 13 ++++ .../domain/feedback/models/PhoneInfo.kt | 7 ++ .../domain/feedback/utils/StringBuilderExt.kt | 5 ++ settings.gradle.kts | 1 + 10 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 domain/feedback/.gitignore create mode 100644 domain/feedback/build.gradle.kts create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/models/PhoneInfo.kt create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/utils/StringBuilderExt.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0ca24d28ab..654eaa2a15 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -64,6 +64,7 @@ dependencies { implementation(projects.domain.analytics) implementation(projects.domain.visa) implementation(projects.domain.onboarding) + implementation(projects.domain.feedback) implementation(projects.common) implementation(projects.core.analytics) diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt index 452eb173aa..7e878f6e69 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt @@ -73,15 +73,9 @@ class SendTransactionFailedEmail( class FeedbackEmail : FeedbackData { override val subjectResId: Int - get() = if (isS2CCard) s2cSubject else tangemSubject - override val mainMessageResId: Int - get() = if (isS2CCard) s2cMainMessage else tangemMainMessage + get() = if (isS2CCard) R.string.feedback_subject_support else R.string.feedback_subject_support_tangem - private val tangemSubject: Int = R.string.feedback_subject_support_tangem - private val tangemMainMessage: Int = R.string.feedback_preface_support - - private val s2cSubject: Int = R.string.feedback_subject_support - private val s2cMainMessage: Int = R.string.feedback_preface_support + override val mainMessageResId: Int = R.string.feedback_preface_support private var isS2CCard = false diff --git a/domain/feedback/.gitignore b/domain/feedback/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/feedback/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/feedback/build.gradle.kts b/domain/feedback/build.gradle.kts new file mode 100644 index 0000000000..83a35acde4 --- /dev/null +++ b/domain/feedback/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.feedback" +} + +dependencies { + implementation(projects.core.res) +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt new file mode 100644 index 0000000000..5023782e64 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -0,0 +1,78 @@ +package com.tangem.domain.feedback + +import com.tangem.domain.feedback.models.BlockchainInfo +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.PhoneInfo +import com.tangem.domain.feedback.utils.breakLine + +internal class FeedbackDataBuilder { + + private val builder = StringBuilder() + + fun addCardInfo(cardInfo: CardInfo) { + builder.appendKeyValue("Card ID", cardInfo.cardId) + builder.appendKeyValue("Firmware version", cardInfo.firmwareVersion) + builder.appendKeyValue("Card Blockchain", cardInfo.cardBlockchain) + builder.appendSignedHashes(cardInfo.signedHashesList) + builder.appendKeyValue("User Wallet ID", cardInfo.userWalletId) + } + + fun addBlockchainInfoList(blockchainInfoList: List) { + blockchainInfoList.forEachBlockchain { isLastIndex -> + builder.appendKeyValue("Blockchain", blockchain) + builder.appendKeyValue("Derivation path", derivationPath) + + // enable later + // if (walletInfo.blockchain == Blockchain.Bitcoin) { + // builder.appendKeyValue("XPUB", infoHolder.extendedPublicKey) + // } + + builder.appendKeyValue("Outputs count", outputsCount) + + if (tokens.isNotEmpty()) { + builder.append("Tokens:") + builder.breakLine() + tokens.forEach { token -> + builder.appendKeyValue("ID", token.id ?: "[custom token]") + builder.appendKeyValue("Name", token.name) + builder.appendKeyValue("Contract address", token.contractAddress) + } + } + + builder.appendKeyValue("Host", host) + builder.appendKeyValue("Wallet address", addresses) + builder.appendKeyValue("Explorer link", explorerLink) + + if (!isLastIndex) builder.appendDelimiter() + } + } + + fun addPhoneInfo(phoneInfo: PhoneInfo) { + builder.appendKeyValue("Phone model", phoneInfo.phoneModel) + builder.appendKeyValue("OS version", phoneInfo.osVersion) + builder.appendKeyValue("App version", phoneInfo.appVersion) + } + + fun addDelimiter(): StringBuilder = builder.appendDelimiter() + + fun build(): String = builder.toString() + + private fun StringBuilder.appendKeyValue(key: String, value: String?) { + if (value.isNullOrBlank()) return + + val keyValuePrefix = if (key.isBlank()) "" else "$key: " + append("$keyValuePrefix$value\n") + } + + private fun StringBuilder.appendSignedHashes(signedHashesList: List) { + signedHashesList.forEach { + appendKeyValue("Signed hashes [${it.curve}]", it.total) + } + } + + private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n") + + private inline fun List.forEachBlockchain(action: BlockchainInfo.(Boolean) -> Unit) { + return forEachIndexed { index, info -> info.action(index == lastIndex) } + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt new file mode 100644 index 0000000000..3daa9d9927 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.feedback.models + +data class BlockchainInfo( + val blockchain: String, + val derivationPath: String, + val outputsCount: String?, + val host: String, + val addresses: String, + val explorerLink: String, + val tokens: List, +) { + + data class TokenInfo( + val id: String?, + val name: String, + val contractAddress: String, + ) +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt new file mode 100644 index 0000000000..79eb5285c6 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.feedback.models + +data class CardInfo( + val userWalletId: String, + val cardId: String, + val firmwareVersion: String, + val cardBlockchain: String?, + val signedHashesList: List, + val isStart2Coin: Boolean, +) { + + data class SignedHashes(val curve: String, val total: String?) +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/PhoneInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/PhoneInfo.kt new file mode 100644 index 0000000000..578d9e2e00 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/PhoneInfo.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.feedback.models + +data class PhoneInfo( + val phoneModel: String, + val osVersion: String, + val appVersion: String, +) \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/StringBuilderExt.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/StringBuilderExt.kt new file mode 100644 index 0000000000..a2e3cb68ee --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/StringBuilderExt.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.feedback.utils + +fun StringBuilder.breakLine(count: Int = 1): StringBuilder = append("\n".repeat(count)) + +fun StringBuilder.skipLine(): StringBuilder = breakLine(count = 2) \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index fb31682f87..bb2e6c46ba 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -138,6 +138,7 @@ include(":domain:transaction") include(":domain:analytics") include(":domain:visa") include(":domain:onboarding") +include(":domain:feedback") // endregion Domain modules // region Data modules From 676936e07374ad9af10c4907f0df3eb2960b0bec Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Mar 2024 15:06:39 +0500 Subject: [PATCH 013/109] Updated on 2026-08-14 --- .../components/inputrow/inner/PasteButton.kt | 10 +++++-- .../ui/fee/SendSpeedAndFeeContent.kt | 27 +------------------ .../presentation/viewmodel/SendViewModel.kt | 24 ++++++++++------- 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt index 1d7d0fab95..88ed54192f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt @@ -36,7 +36,12 @@ import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) { val clipboardManager = LocalClipboardManager.current val hapticFeedback = LocalHapticFeedback.current - + val isPasteEnabled = !clipboardManager.getText()?.text.isNullOrEmpty() + val color = if (isPasteEnabled) { + TangemTheme.colors.button.primary + } else { + TangemTheme.colors.button.secondary + } AnimatedVisibility( visible = isPasteButtonVisible, label = "Paste Button Visibility Animation", @@ -50,7 +55,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi color = TangemTheme.colors.text.primary2, modifier = Modifier .background( - color = TangemTheme.colors.button.primary, + color = color, shape = TangemTheme.shapes.roundedCornersXMedium, ) .padding( @@ -60,6 +65,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(radius = TangemTheme.dimens.radius8), + enabled = isPasteEnabled, onClick = { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) onClick( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt index 1695763049..444d03e9a6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -18,7 +18,6 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY" private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY" @@ -37,9 +36,8 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S ), ) { feeSelector(state, clickIntents) - topNotifications(notifications) customFee(feeSendState) - middleNotifications(notifications) + notifications(notifications) } } @@ -56,29 +54,6 @@ private fun LazyListScope.feeSelector(state: SendStates.FeeState, clickIntents: } } -private fun LazyListScope.topNotifications( - configs: ImmutableList, - modifier: Modifier = Modifier, -) { - notifications( - configs = configs.filter { - it is SendFeeNotification.Error.ExceedsBalance || - it is SendFeeNotification.Warning.NetworkFeeUnreachable - }.toImmutableList(), - modifier = modifier, - ) -} - -private fun LazyListScope.middleNotifications( - configs: ImmutableList, - modifier: Modifier = Modifier, -) { - notifications( - configs = configs.filterIsInstance().toImmutableList(), - modifier = modifier, - ) -} - @OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.notifications( configs: ImmutableList, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 1b356af2b9..30b3e00367 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -1,4 +1,3 @@ - package com.tangem.features.send.impl.presentation.viewmodel import androidx.compose.runtime.getValue @@ -308,15 +307,22 @@ internal class SendViewModel @Inject constructor( coinCryptoCurrencyStatus = coinCurrencyStatus feeCryptoCurrencyStatus = feeCurrencyStatus - if (transactionId != null && amount != null && destinationAddress != null) { - uiState = stateFactory.getReadyState(amount, destinationAddress) - stateRouter.showFee() - } else { - getWalletsAndRecent() - uiState = stateFactory.getReadyState() - stateRouter.showRecipient() + when { + uiState.sendState.isSuccess -> { + stateRouter.showSend() + } + transactionId != null && amount != null && destinationAddress != null -> { + uiState = stateFactory.getReadyState(amount, destinationAddress) + stateRouter.showFee() + updateNotifications() + } + else -> { + getWalletsAndRecent() + uiState = stateFactory.getReadyState() + stateRouter.showRecipient() + updateNotifications() + } } - updateNotifications() } private fun getWalletsAndRecent() { From eee206bec187b7a21c3f39bee93814d057c38b33 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Mar 2024 17:50:56 +0100 Subject: [PATCH 014/109] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 1 - core/res/src/main/res/values-fr/strings.xml | 1 - core/res/src/main/res/values-it/strings.xml | 1 - core/res/src/main/res/values-ru/strings.xml | 24 ++++++++++++------- .../src/main/res/values-zh-rTW/strings.xml | 10 +++----- core/res/src/main/res/values/strings.xml | 20 ++++++++-------- .../managetokens/ui/components/TokensList.kt | 2 +- .../feature/swap/models/SwapStateHolder.kt | 1 - .../tangem/feature/swap/ui/StateBuilder.kt | 16 ++++--------- .../feature/swap/ui/SwapScreenContent.kt | 9 ++----- 10 files changed, 36 insertions(+), 49 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 15fe23fdca..559c09d33c 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -50,7 +50,6 @@ %s wird gesendet Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert Ungültige Adresse - Tangem Wallet Tangem Twin WalletConnect Die Adresse wurde erfolgreich kopiert diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 0901cc1225..e7454edeb7 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -50,7 +50,6 @@ Sera envoyé %s La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps Adresse incorrecte - Tangem Wallet Tangem Twin WalletConnect L\'adresse a été copiée avec succès diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 2c944c1c4a..e7caa7829e 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -50,7 +50,6 @@ Sarà inviato %s La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo Indirizzo non valido - Tangem Wallet Tangem Twin WalletConnect L\'indirizzo è stato copiato con successo diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 8474f7f533..92eb872a29 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -15,6 +15,7 @@ Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки. У вас возникли трудности со сканированием карты? Эта карта не предназначена для работы с этим приложением + Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться. Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem Включите биометрическую аутентификацию Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком. @@ -139,6 +140,7 @@ Адрес контракта некорректен Путь деривации некорректен Пожалуйста, выберите сеть + Десятичное число должно быть действительным целым числом, до %li Своя деривация Например m/00\'/0000\'/0\'/0/0 Введите свою деривацию @@ -185,7 +187,6 @@ У вас нет добавленных токенов. Добавьте токены для обмена Недоступен для обмена с %s Предоставлено - Предоставлено %s Статус Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами Выберите провайдера @@ -262,6 +263,7 @@ Токены Добавить Изменить + Рыночная капитализация Основная сеть Не основной или основной блокчейн, на котором размещен токен Не основные сети @@ -275,7 +277,6 @@ %1$d из %2$d кошельков например Bitcoin - Рыночная капитализация Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. Голосовать Выберите кошелек @@ -320,7 +321,19 @@ Резервная копия Прочитать о seed-фразе Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте. + + + Запишите эти %d слова в порядке, указанном ниже, и сохраните их в надежном месте. + Запишите эти %d слов в порядке, указанном ниже, и сохраните их в надежном месте. + Запишите эти %d слов в порядке, указанном ниже, и сохраните их в надежном месте. + Ваша seed-фраза + + + %d слова + %d слов + %d слов + Чтобы импортировать кошелек, введите seed-фразу в поле ниже Создать seed-фразу Импорт кошелька @@ -484,10 +497,6 @@ %s будет отправлено Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время Неверный адрес - Купить сейчас - У меня есть промо-код… - Tangem Wallet - Другие способы оплаты Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Революционный аппаратный кошелек До трех карт с одним кошельком @@ -509,14 +518,11 @@ Комиссии Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. Подтвердить - Ошибка: %s Вы отправляете Дать разрешение Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. Недостаточно средств Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии - Транзакция в процессе… - Подождите Подтвердить Текущая транзакция Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index b5cdf09a4e..3b90b9b331 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -185,6 +185,9 @@ 創建備份 閱讀更多關於助記詞的訊息 按照下面給出的順序寫下這 12 個單詞,並將它們存放在安全秘密的地方。 + + 按照下面給出的順序寫下這 %d 個單詞,並將它們存放在安全秘密的地方。 + 生成助記詞 導入錢包 使用助記詞 @@ -272,10 +275,6 @@ %s 將被發送 交易已成功簽署並發送至區塊鏈節點。錢包餘額稍後更新 無效地址 - 現在購買 - 輸入折扣碼 - Tangem Wallet - 其他付款方式 安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中 創新式的硬體錢包 最多 3張實體卡片 到一個錢包 @@ -292,12 +291,9 @@ 兼容Web3.0 批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。 批准 - 錯誤: %s 賦予權限 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量 餘額不足 - 交易進行中... - 等待中 允許 賦予權限 數量 %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index d07bb17a98..53492c3a02 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -15,6 +15,8 @@ Tokens in %1$s network are not supported by this card due to firmware limitation. Are you having difficulty scanning your card? This card is not designed to work with this app + Default Fee + Enable Default Fee to set transaction fees automatically and skip the Fee page when sending funds. You can always go back to this page if necessary. Go to settings to enable biometric authentication in the Tangem App Enable biometric authentication This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. @@ -185,7 +187,6 @@ You haven\'t added any tokens yet. Add tokens via Market to swap Cannot be swapped for %s Provided by - Provided by %s Status Tangem offers token swaps via 3rd-party providers according to each provider\'s terms Choose provider @@ -263,7 +264,6 @@ Tokens Add Edit - Couldn’t find this token, you can add it manually Coin market cap Blockchain the cryptocurrency was initially created Native network @@ -278,7 +278,6 @@ %1$d of %2$d wallets e.g. BTC I trust, hodl I must - Coin market cap The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it. Upvote Choose wallet @@ -323,7 +322,15 @@ Creating a backup Read more about seed phrase Write these 12 words down in the order given below and store them in a safe and secret place. + + + Write these %d words down in the order given below and store them in a safe and secret place. + Your seed phrase + + + %d words + To import your wallet, enter your seed phrase in the field below Generate seed phrase Import wallet @@ -499,10 +506,6 @@ Invalid address %1$s (%2$s) Transaction sent - Buy now - I have a promo code… - Tangem Wallet - Other payment methods Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet Up to 3 physical cards to one wallet @@ -524,14 +527,11 @@ Fees All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval. Approve - Error: %s You swap Give Permission Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Sending amount will be reduced to cover the selected fee level - Transaction in progress... - Waiting Approve Current transaction The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt index d6c2b505ea..ca26ef0621 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt @@ -25,7 +25,7 @@ internal fun TokensList( LazyColumn(modifier = modifier) { item { Text( - text = stringResource(id = R.string.manage_tokens_title), + text = stringResource(id = R.string.manage_tokens_list_header_title), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 32b1388ef6..cff24ca2a6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -108,7 +108,6 @@ sealed interface SwapWarning { val title: TextReference? = null, val message: TextReference? = null, val type: GenericWarningType = GenericWarningType.OTHER, - val shouldWrapMessage: Boolean = false, val onClick: () -> Unit, ) : SwapWarning data class GeneralError(val notificationConfig: NotificationConfig) : SwapWarning diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index cea2fca9cc..1580582c2f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -344,8 +344,8 @@ internal class StateBuilder( if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { warnings.add( SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.swapping_pending_transaction_title), - description = resourceReference(R.string.swapping_pending_transaction_subtitle), + title = resourceReference(R.string.warning_express_approval_in_progress_title), + description = resourceReference(R.string.warning_express_approval_in_progress_message), ), ) } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { @@ -821,8 +821,8 @@ internal class StateBuilder( warnings.add( 0, SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.swapping_pending_transaction_title), - description = resourceReference(R.string.swapping_pending_transaction_subtitle), + title = resourceReference(R.string.warning_express_approval_in_progress_title), + description = resourceReference(R.string.warning_express_approval_in_progress_message), ), ) return uiState.copy( @@ -926,17 +926,11 @@ internal class StateBuilder( fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(alert = null) - fun addWarning( - uiState: SwapStateHolder, - message: TextReference?, - shouldWrapMessage: Boolean = false, - onClick: () -> Unit, - ): SwapStateHolder { + fun addWarning(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder { val renewWarnings = uiState.warnings.filterNot { it is SwapWarning.GenericWarning }.toMutableList() renewWarnings.add( SwapWarning.GenericWarning( message = message, - shouldWrapMessage = shouldWrapMessage, onClick = onClick, ), ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 6c31dea226..64964aef80 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -337,13 +337,8 @@ private fun SwapWarnings(warnings: List) { ) } is SwapWarning.GenericWarning -> { - val message = warning.message?.let { - if (warning.shouldWrapMessage) { - String.format(stringResource(id = R.string.swapping_error_wrapper), it.resolveReference()) - } else { - it.resolveReference() - } - } ?: stringResource(id = R.string.common_unknown_error) + val message = warning.message?.resolveReference() + ?: stringResource(id = R.string.common_unknown_error) RefreshableWaringCard( title = stringResource(id = R.string.common_warning), description = message, From 675d13ab6f1543f6d2ad8d45da7529d43ecc2231 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 9 Mar 2024 09:51:23 +0800 Subject: [PATCH 015/109] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + data/feedback/.gitignore | 1 + data/feedback/build.gradle.kts | 42 +++++++ .../feedback/DefaultFeedbackRepository.kt | 104 ++++++++++++++++++ .../converters/BlockchainInfoConverter.kt | 43 ++++++++ .../feedback/converters/CardInfoConverter.kt | 29 +++++ .../feedback/di/FeedbackRepositoryModule.kt | 30 +++++ .../settings/DefaultSettingsRepository.kt | 7 -- .../domain/feedback/FeedbackDataBuilder.kt | 33 +++++- .../domain/feedback/models/AppLogModel.kt | 3 + .../domain/feedback/models/BlockchainInfo.kt | 14 ++- .../feedback/repository/FeedbackRepository.kt | 20 ++++ .../domain/settings/models/AppLogsModel.kt | 3 - .../repositories/SettingsRepository.kt | 5 - settings.gradle.kts | 1 + 15 files changed, 316 insertions(+), 20 deletions(-) create mode 100644 data/feedback/.gitignore create mode 100644 data/feedback/build.gradle.kts create mode 100644 data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt create mode 100644 data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt create mode 100644 data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt create mode 100644 data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/models/AppLogModel.kt create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt delete mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/models/AppLogsModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 654eaa2a15..4543073e2a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -94,6 +94,7 @@ dependencies { implementation(projects.data.visa) implementation(projects.data.promo) implementation(projects.data.onboarding) + implementation(projects.data.feedback) /** Features */ implementation(projects.features.onboarding) diff --git a/data/feedback/.gitignore b/data/feedback/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/feedback/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/feedback/build.gradle.kts b/data/feedback/build.gradle.kts new file mode 100644 index 0000000000..306603b7c2 --- /dev/null +++ b/data/feedback/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.feedback" +} + +dependencies { + + // region AndroidX libraries + implementation(deps.androidx.datastore) + // endregion + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // endregion + + // region Tangem libraries + implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) + // endregion + + // Other libraries + implementation(deps.timber) + // endregion + + // region Core modules + implementation(projects.core.datasource) + // endregion + + implementation(projects.core.utils) + + implementation(projects.domain.feedback) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.wallets.models) +} \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt new file mode 100644 index 0000000000..580c5fb4ab --- /dev/null +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -0,0 +1,104 @@ +package com.tangem.data.feedback + +import android.content.Context +import android.content.pm.PackageInfo +import android.os.Build +import com.tangem.data.feedback.converters.BlockchainInfoConverter +import com.tangem.data.feedback.converters.CardInfoConverter +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectMap +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.feedback.models.AppLogModel +import com.tangem.domain.feedback.models.BlockchainInfo +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.PhoneInfo +import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.wallets.models.UserWallet +import timber.log.Timber +import java.io.File +import java.io.FileWriter +import java.io.StringWriter + +/** + * Implementation of [FeedbackRepository] + * + * @property appPreferencesStore application preferences store + * @property userWalletsStore user wallets store + * @property walletManagersStore wallet managers store + * @property context context for getting app version + * +[REDACTED_AUTHOR] + */ +internal class DefaultFeedbackRepository( + private val appPreferencesStore: AppPreferencesStore, + private val userWalletsStore: UserWalletsStore, + private val walletManagersStore: WalletManagersStore, + private val context: Context, +) : FeedbackRepository { + + override suspend fun getCardInfo(): CardInfo { + return CardInfoConverter.convert(value = getSelectedUserWallet()) + } + + override suspend fun getBlockchainInfoList(): List { + return walletManagersStore + .getAllSync(userWalletId = getSelectedUserWallet().walletId) + .map(BlockchainInfoConverter::convert) + } + + override fun getPhoneInfo(): PhoneInfo { + return PhoneInfo( + phoneModel = Build.MODEL, + osVersion = Build.VERSION.SDK_INT.toString(), + appVersion = getAppVersion(), + ) + } + + override suspend fun getAppLogs(): List { + return appPreferencesStore.getObjectMap(key = PreferencesKeys.APP_LOGS_KEY) + .map { AppLogModel(timestamp = it.key.toLong(), message = it.value) } + } + + override suspend fun createLogFile(logs: List): File? { + return try { + val file = File(context.filesDir, LOGS_FILE) + file.delete() + file.createNewFile() + + val stringWriter = StringWriter() + + logs.forEach(stringWriter::append) + + val fileWriter = FileWriter(file) + fileWriter.write(stringWriter.toString()) + fileWriter.close() + + file + } catch (ex: Exception) { + Timber.e(ex, "Logs file isn't created") + null + } + } + + private fun getAppVersion(): String { + return runCatching { context.packageManager.getPackageInfo(context.packageName, 0) } + .fold( + onSuccess = PackageInfo::versionName, + onFailure = { + Timber.e(it) + "x.y.z" + }, + ) + } + + private fun getSelectedUserWallet(): UserWallet { + return userWalletsStore.selectedUserWalletOrNull + ?: error("UserWallet is not selected") + } + + private companion object { + const val LOGS_FILE = "logs.txt" + } +} \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt new file mode 100644 index 0000000000..de7f9b6d04 --- /dev/null +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt @@ -0,0 +1,43 @@ +package com.tangem.data.feedback.converters + +import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.address.Address +import com.tangem.domain.feedback.models.BlockchainInfo +import com.tangem.domain.feedback.models.BlockchainInfo.Addresses.Multiple.AddressInfo +import com.tangem.utils.converter.Converter +import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses + +/** + * Converter from [WalletManager] to [BlockchainInfo] + * +[REDACTED_AUTHOR] + */ +internal object BlockchainInfoConverter : Converter { + + override fun convert(value: WalletManager): BlockchainInfo { + return BlockchainInfo( + blockchain = value.wallet.blockchain.fullName, + derivationPath = value.wallet.publicKey.derivationPath?.rawPath ?: "", + outputsCount = value.outputsCount?.toString(), + host = value.currentHost, + addresses = value.wallet.mapAddresses(Address::value), + explorerLinks = value.wallet.mapAddresses { value.wallet.getExploreUrl(it.value) }, + tokens = value.cardTokens.map { token -> + BlockchainInfo.TokenInfo(id = token.id, name = token.name, contractAddress = token.contractAddress) + }, + ) + } + + private inline fun Wallet.mapAddresses(map: (Address) -> String): BlockchainAddresses { + return if (addresses.size == 1) { + BlockchainAddresses.Single(value = map(addresses.first())) + } else { + BlockchainAddresses.Multiple( + addresses.map { + AddressInfo(type = it.type.name, value = map(it)) + }, + ) + } + } +} \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt new file mode 100644 index 0000000000..e9a1a61019 --- /dev/null +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.feedback.converters + +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.utils.converter.Converter + +/** + * Converter from [UserWallet] to [CardInfo] + * +[REDACTED_AUTHOR] + */ +internal object CardInfoConverter : Converter { + + override fun convert(value: UserWallet): CardInfo { + return with(value.scanResponse) { + CardInfo( + userWalletId = value.walletId.stringValue, + cardId = card.cardId, + firmwareVersion = card.firmwareVersion.stringValue, + cardBlockchain = walletData?.blockchain, + signedHashesList = card.wallets.map { + CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString()) + }, + isStart2Coin = value.scanResponse.card.isStart2Coin, + ) + } + } +} \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt new file mode 100644 index 0000000000..5a00938959 --- /dev/null +++ b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt @@ -0,0 +1,30 @@ +package com.tangem.data.feedback.di + +import android.content.Context +import com.tangem.data.feedback.DefaultFeedbackRepository +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.feedback.repository.FeedbackRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object FeedbackRepositoryModule { + + @Provides + @Singleton + fun provideFeedbackRepository( + appPreferencesStore: AppPreferencesStore, + userWalletsStore: UserWalletsStore, + walletManagersStore: WalletManagersStore, + @ApplicationContext context: Context, + ): FeedbackRepository { + return DefaultFeedbackRepository(appPreferencesStore, userWalletsStore, walletManagersStore, context) + } +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 4e7ff03252..308e823b90 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -3,10 +3,8 @@ package com.tangem.data.settings import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store -import com.tangem.domain.settings.models.AppLogsModel import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -36,11 +34,6 @@ internal class DefaultSettingsRepository( ) } - override suspend fun getAppLogs(): List { - return appPreferencesStore.getObjectMap(key = PreferencesKeys.APP_LOGS_KEY) - .map { AppLogsModel(timestamp = it.key.toLong(), message = it.value) } - } - override suspend fun updateAppLogs(message: String) { val newLogs = DateTime.now().millis.toString() to message diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index 5023782e64..35740e1357 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -4,6 +4,7 @@ import com.tangem.domain.feedback.models.BlockchainInfo import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.feedback.models.PhoneInfo import com.tangem.domain.feedback.utils.breakLine +import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses internal class FeedbackDataBuilder { @@ -40,8 +41,16 @@ internal class FeedbackDataBuilder { } builder.appendKeyValue("Host", host) - builder.appendKeyValue("Wallet address", addresses) - builder.appendKeyValue("Explorer link", explorerLink) + + builder.appendAddresses( + key = "Wallet address${addresses.isMultiple(suffix = "es")}", + addresses = addresses, + ) + + builder.appendAddresses( + key = "Explorer link${explorerLinks.isMultiple(suffix = "s")}", + addresses = explorerLinks, + ) if (!isLastIndex) builder.appendDelimiter() } @@ -55,7 +64,7 @@ internal class FeedbackDataBuilder { fun addDelimiter(): StringBuilder = builder.appendDelimiter() - fun build(): String = builder.toString() + fun build(): String = builder.trimEnd().toString() private fun StringBuilder.appendKeyValue(key: String, value: String?) { if (value.isNullOrBlank()) return @@ -70,6 +79,24 @@ internal class FeedbackDataBuilder { } } + private fun StringBuilder.appendAddresses(key: String, addresses: BlockchainAddresses) { + appendKeyValue( + key = key, + value = when (addresses) { + is BlockchainInfo.Addresses.Multiple -> { + addresses.values.joinToString(separator = "\n", prefix = "\n") { + "${it.type} — ${it.value}" + } + } + is BlockchainInfo.Addresses.Single -> addresses.value + }, + ) + } + + private fun BlockchainAddresses.isMultiple(suffix: String): String { + return if (this is BlockchainAddresses.Multiple) suffix else "" + } + private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n") private inline fun List.forEachBlockchain(action: BlockchainInfo.(Boolean) -> Unit) { diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/AppLogModel.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/AppLogModel.kt new file mode 100644 index 0000000000..6688c29a1f --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/AppLogModel.kt @@ -0,0 +1,3 @@ +package com.tangem.domain.feedback.models + +data class AppLogModel(val timestamp: Long, val message: String) \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt index 3daa9d9927..da0a0df2a5 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt @@ -5,11 +5,21 @@ data class BlockchainInfo( val derivationPath: String, val outputsCount: String?, val host: String, - val addresses: String, - val explorerLink: String, + val addresses: Addresses, + val explorerLinks: Addresses, val tokens: List, ) { + sealed class Addresses { + + data class Single(val value: String) : Addresses() + + data class Multiple(val values: List) : Addresses() { + + data class AddressInfo(val type: String, val value: String) + } + } + data class TokenInfo( val id: String?, val name: String, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt new file mode 100644 index 0000000000..246afde319 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.feedback.repository + +import com.tangem.domain.feedback.models.AppLogModel +import com.tangem.domain.feedback.models.BlockchainInfo +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.PhoneInfo +import java.io.File + +interface FeedbackRepository { + + suspend fun getCardInfo(): CardInfo + + suspend fun getBlockchainInfoList(): List + + fun getPhoneInfo(): PhoneInfo + + suspend fun getAppLogs(): List + + suspend fun createLogFile(logs: List): File? +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/models/AppLogsModel.kt b/domain/settings/src/main/java/com/tangem/domain/settings/models/AppLogsModel.kt deleted file mode 100644 index 746974930f..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/models/AppLogsModel.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.domain.settings.models - -data class AppLogsModel(val timestamp: Long, val message: String) \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index f686561f3e..48e6bb5ca2 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -1,7 +1,5 @@ package com.tangem.domain.settings.repositories -import com.tangem.domain.settings.models.AppLogsModel - interface SettingsRepository { suspend fun shouldShowSaveUserWalletScreen(): Boolean @@ -10,9 +8,6 @@ interface SettingsRepository { suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) - @Throws - suspend fun getAppLogs(): List - @Throws suspend fun updateAppLogs(message: String) diff --git a/settings.gradle.kts b/settings.gradle.kts index bb2e6c46ba..a7380d73d8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -157,4 +157,5 @@ include(":data:transaction") include(":data:visa") include(":data:promo") include(":data:onboarding") +include(":data:feedback") // endregion Data modules \ No newline at end of file From 742795d2d9eb0775fc940e6367d28d4d0ba24738 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Mar 2024 11:51:19 +0100 Subject: [PATCH 016/109] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/Warnings.kt | 4 +- .../swap/domain/models/domain/Warning.kt | 2 + .../feature/swap/domain/SwapInteractor.kt | 2 - .../feature/swap/domain/SwapInteractorImpl.kt | 32 ++-- features/swap/presentation/build.gradle.kts | 1 + .../feature/swap/models/SwapStateHolder.kt | 3 + .../tangem/feature/swap/models/UiActions.kt | 3 + .../tangem/feature/swap/ui/StateBuilder.kt | 181 +++++++++++++++--- .../feature/swap/ui/SwapScreenContent.kt | 13 +- .../feature/swap/viewmodels/SwapViewModel.kt | 26 ++- 10 files changed, 205 insertions(+), 62 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt index d0d3315f20..fe2a681d08 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt @@ -102,7 +102,7 @@ fun ClickableWarningCard( * >Figma component */ @Composable -fun RefreshableWaringCard( +fun RefreshableWarningCard( title: String, description: String, icon: @Composable (() -> Unit)? = null, @@ -193,7 +193,7 @@ private fun WarningsPreview() { onClick = {}, ) SpacerH32() - RefreshableWaringCard( + RefreshableWarningCard( title = "Exchange rate has expired", description = "To access all the networks, you need to scan the card.", onClick = {}, diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt index d7f7790479..318b5635de 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt @@ -7,4 +7,6 @@ sealed class Warning { data class ExistentialDepositWarning(val existentialDeposit: BigDecimal) : Warning() data class MinAmountWarning(val dustValue: BigDecimal) : Warning() + + data class ReduceAmountWarning(val tezosFeeThreshold: BigDecimal) : Warning() } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index d3ec1058b9..b43322b46a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -79,8 +79,6 @@ interface SwapInteractor { */ fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount - fun isAvailableToSwap(networkId: String): Boolean - fun getSelectedWallet(): UserWallet? suspend fun selectInitialCurrencyToSwap( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index c1a72132a4..74d9cdd518 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -197,7 +197,6 @@ internal class SwapInteractorImpl @Inject constructor( return repository.getPairs(initialCurrency, currenciesList) } - @Deprecated("used in old swap mechanism") override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState { val derivationPath = permissionOptions.fromToken.network.derivationPath.value val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) { @@ -241,7 +240,6 @@ internal class SwapInteractorImpl @Inject constructor( } } - @Deprecated("used in old swap mechanism") override suspend fun findBestQuote( fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, @@ -376,6 +374,7 @@ internal class SwapInteractorImpl @Inject constructor( val warnings = mutableListOf() manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken) manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount) + manageReduceAmountWarning(warnings, fromTokenStatus, amount) return warnings } @@ -424,6 +423,17 @@ internal class SwapInteractorImpl @Inject constructor( } } + private fun manageReduceAmountWarning( + warnings: MutableList, + fromTokenStatus: CryptoCurrencyStatus, + amount: SwapAmount, + ) { + val isTezos = fromTokenStatus.currency.network.id.value == Blockchain.Tezos.id + if (isTezos && amount.value == fromTokenStatus.value.amount) { + warnings.add(Warning.ReduceAmountWarning(TEZOS_FEE_THRESHOLD)) + } + } + override suspend fun onSwap( swapProvider: SwapProvider, swapData: SwapDataModel?, @@ -729,16 +739,10 @@ internal class SwapInteractorImpl @Inject constructor( ) } - @Deprecated("used in old swap mechanism") override fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount { return SwapAmount(token.value.amount ?: BigDecimal.ZERO, token.currency.decimals) } - @Deprecated("used in old swap mechanism") - override fun isAvailableToSwap(networkId: String): Boolean { - return ONE_INCH_SUPPORTED_NETWORKS.contains(networkId) - } - override suspend fun selectInitialCurrencyToSwap( initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, @@ -1575,16 +1579,6 @@ internal class SwapInteractorImpl @Inject constructor( private const val INCREASE_GAS_LIMIT_BY = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% private const val INFINITY_SYMBOL = "∞" - - private val ONE_INCH_SUPPORTED_NETWORKS = listOf( - "ethereum", - "binance-smart-chain", - "polygon-pos", - "optimistic-ethereum", - "arbitrum-one", - "xdai", - "avalanche", - "fantom", - ) + private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01") } } \ No newline at end of file diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 8986bf7e0d..c78375b5e4 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -67,4 +67,5 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index cff24ca2a6..e5db542bba 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -20,6 +20,7 @@ data class SwapStateHolder( val alert: SwapWarning.GenericWarning? = null, val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED, val providerState: ProviderState, + val reduceAmountIgnore: Boolean, // ignore warning about reducing XTZ amount by 0.01 val fee: FeeItemState = FeeItemState.Empty, val permissionState: SwapPermissionState = SwapPermissionState.Empty, @@ -115,6 +116,8 @@ sealed interface SwapWarning { data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning data class GeneralInformational(val notificationConfig: NotificationConfig) : SwapWarning data class TransactionInProgressWarning(val title: TextReference, val description: TextReference) : SwapWarning + data class NeedReserveToCreateAccount(val notificationConfig: NotificationConfig) : SwapWarning + data class ReduceAmount(val notificationConfig: NotificationConfig) : SwapWarning } enum class GenericWarningType { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index e7641eca69..bd84a05cac 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.models import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.ui.TxFee data class UiActions( @@ -13,6 +14,8 @@ data class UiActions( val onChangeCardsClicked: () -> Unit, val onBackClicked: () -> Unit, val onMaxAmountSelected: () -> Unit, + val onReduceAmount: (SwapAmount) -> Unit, + val onReduceAmountIgnoreClick: () -> Unit, val openPermissionBottomSheet: () -> Unit, val onChangeApproveType: (ApproveType) -> Unit, // region new actions diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 1580582c2f..2587562721 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -88,6 +88,7 @@ internal class StateBuilder( onShowPermissionBottomSheet = actions.openPermissionBottomSheet, providerState = ProviderState.Empty(), shouldShowMaxAmount = false, + reduceAmountIgnore = false, priceImpact = PriceImpact.Empty(), ) } @@ -213,7 +214,11 @@ internal class StateBuilder( ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder - val warnings = getWarningsForSuccessState(quoteModel, fromToken) + val warnings = getWarningsForSuccessState( + quoteModel = quoteModel, + fromToken = fromToken, + ignoreAmountReduce = uiStateHolder.reduceAmountIgnore, + ) val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus @@ -311,36 +316,23 @@ internal class StateBuilder( private fun getWarningsForSuccessState( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, + ignoreAmountReduce: Boolean, ): List { val warnings = mutableListOf() - addDomainWarnings(quoteModel, warnings) - if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && - quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough && - quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest - ) { - warnings.add( - SwapWarning.PermissionNeeded( - createPermissionNotificationConfig(fromToken.symbol), - ), - ) - } - when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { - is IncludeFeeInAmount.Included -> - warnings.add( - SwapWarning.GeneralWarning( - createNetworkFeeCoverageNotificationConfig(), - ), - ) - else -> Unit - } - addUnableCoverFeeWarning(quoteModel, fromToken, warnings) - // check isBalanceEnough, but for dex includeFeeInAmount always Excluded - if (!quoteModel.preparedSwapConfigState.isBalanceEnough && - quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included - ) { - warnings.add(SwapWarning.InsufficientFunds) - } + maybeAddDomainWarnings(quoteModel, warnings, ignoreAmountReduce) + maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings) + maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken) + maybeAddNetworkFeeCoverageWarning(quoteModel, warnings) + maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings) + maybeAddInsufficientFundsWarning(quoteModel, warnings) + maybeAddTransactionInProgressWarning(quoteModel, warnings) + return warnings + } + private fun maybeAddTransactionInProgressWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ) { if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { warnings.add( SwapWarning.TransactionInProgressWarning( @@ -361,10 +353,13 @@ internal class StateBuilder( ), ) } - return warnings } - private fun addDomainWarnings(quoteModel: SwapState.QuotesLoadedState, warnings: MutableList) { + private fun maybeAddDomainWarnings( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ignoreAmountReduce: Boolean, + ) { quoteModel.warnings.forEach { when (it) { is Warning.ExistentialDepositWarning -> { @@ -401,11 +396,84 @@ internal class StateBuilder( ), ) } + is Warning.ReduceAmountWarning -> { + if (!ignoreAmountReduce) { + warnings.add( + SwapWarning.ReduceAmount( + notificationConfig = createReduceAmountNotificationConfig( + amount = it.tezosFeeThreshold.toPlainString(), + onConfirmClick = { + val fromAmount = quoteModel.fromTokenInfo.tokenAmount + val patchedAmount = fromAmount.copy( + value = fromAmount.value - it.tezosFeeThreshold, + ) + actions.onReduceAmount(patchedAmount) + }, + onDismissClick = actions.onReduceAmountIgnoreClick, + ), + ), + ) + } + } } } } - private fun addUnableCoverFeeWarning( + private fun maybeAddNeedReserveToCreateAccountWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ) { + val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value + if (status is CryptoCurrencyStatus.NoAccount) { + val amount = quoteModel.toTokenInfo.tokenAmount.value + val amountToCreateAccount = status.amountToCreateAccount + + if (amount < amountToCreateAccount) { + warnings.add( + SwapWarning.NeedReserveToCreateAccount( + notificationConfig = createActivateAccountNotificationConfig( + status.amountToCreateAccount, + quoteModel.toTokenInfo.cryptoCurrencyStatus.currency.name, + ), + ), + ) + } + } + } + + private fun maybeAddPermissionNeededWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + fromToken: CryptoCurrency, + ) { + if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && + quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough && + quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest + ) { + warnings.add( + SwapWarning.PermissionNeeded( + createPermissionNotificationConfig(fromToken.symbol), + ), + ) + } + } + + private fun maybeAddNetworkFeeCoverageWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ) { + when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { + is IncludeFeeInAmount.Included -> + warnings.add( + SwapWarning.GeneralWarning( + createNetworkFeeCoverageNotificationConfig(), + ), + ) + else -> Unit + } + } + + private fun maybeAddUnableCoverFeeWarning( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, warnings: MutableList, @@ -428,7 +496,29 @@ internal class StateBuilder( } } + private fun maybeAddInsufficientFundsWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ) { + // check isBalanceEnough, but for dex includeFeeInAmount always Excluded + if (!quoteModel.preparedSwapConfigState.isBalanceEnough && + quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included + ) { + warnings.add(SwapWarning.InsufficientFunds) + } + } + private fun getSwapButtonEnabled(quoteModel: SwapState.QuotesLoadedState): Boolean { + val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value + if (status is CryptoCurrencyStatus.NoAccount) { + val amount = quoteModel.toTokenInfo.tokenAmount.value + val amountToCreateAccount = status.amountToCreateAccount + + if (amount < amountToCreateAccount) { + return false + } + } + val preparedSwapConfigState = quoteModel.preparedSwapConfigState // check has has outgoing transaction if (preparedSwapConfigState.hasOutgoingTransaction) return false @@ -1212,6 +1302,35 @@ internal class StateBuilder( ) } + private fun createActivateAccountNotificationConfig(amount: BigDecimal, token: String): NotificationConfig { + return NotificationConfig( + title = resourceReference( + id = R.string.send_notification_invalid_reserve_amount_title, + formatArgs = wrappedList("$amount $token"), + ), + subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text), + iconResId = R.drawable.img_attention_20, + ) + } + + private fun createReduceAmountNotificationConfig( + amount: String, + onConfirmClick: () -> Unit, + onDismissClick: () -> Unit, + ): NotificationConfig { + return NotificationConfig( + title = resourceReference(R.string.send_notification_high_fee_title), + subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)), + iconResId = R.drawable.img_attention_20, + buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig( + primaryText = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)), + onPrimaryClick = onConfirmClick, + secondaryText = resourceReference(R.string.xtz_withdrawal_message_ignore), + onSecondaryClick = onDismissClick, + ), + ) + } + private fun createUnableToCoverFeeNotificationConfig( fromToken: CryptoCurrency, feeCurrency: CryptoCurrency?, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 64964aef80..fa48e3f6fb 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -339,7 +339,7 @@ private fun SwapWarnings(warnings: List) { is SwapWarning.GenericWarning -> { val message = warning.message?.resolveReference() ?: stringResource(id = R.string.common_unknown_error) - RefreshableWaringCard( + RefreshableWarningCard( title = stringResource(id = R.string.common_warning), description = message, onClick = warning.onClick, @@ -372,6 +372,16 @@ private fun SwapWarnings(warnings: List) { iconTint = TangemTheme.colors.icon.accent, ) } + is SwapWarning.NeedReserveToCreateAccount -> { + Notification( + config = warning.notificationConfig, + ) + } + is SwapWarning.ReduceAmount -> { + Notification( + config = warning.notificationConfig, + ) + } is SwapWarning.TransactionInProgressWarning -> { CardWithIcon( title = warning.title.resolveReference(), @@ -494,6 +504,7 @@ private val state = SwapStateHolder( providerState = ProviderState.Loading(), priceImpact = PriceImpact.Empty(), shouldShowMaxAmount = true, + reduceAmountIgnore = false, tosState = TosState( tosLink = LegalState( title = stringReference("Terms of Use"), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index a559fb5054..2077c47764 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -27,6 +27,7 @@ import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.ExpressException +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* @@ -810,6 +811,10 @@ internal class SwapViewModel @Inject constructor( } } + private fun onReduceAmountClicked(newAmount: SwapAmount) { + onAmountChanged(newAmount.formatToUIRepresentation()) + } + @Suppress("UnusedPrivateMember") private fun onAmountSelected(selected: Boolean) { if (selected) { @@ -872,7 +877,14 @@ internal class SwapViewModel @Inject constructor( } onSearchEntered("") }, - onMaxAmountSelected = { onMaxAmountClicked() }, + onMaxAmountSelected = ::onMaxAmountClicked, + onReduceAmount = ::onReduceAmountClicked, + onReduceAmountIgnoreClick = { + uiState = uiState.copy( + reduceAmountIgnore = true, + warnings = uiState.warnings.filter { it !is SwapWarning.ReduceAmount }, + ) + }, openPermissionBottomSheet = { singleTaskScheduler.cancelTask() analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) @@ -1151,11 +1163,11 @@ internal class SwapViewModel @Inject constructor( ) } - companion object { - private const val loggingTag = "SwapViewModel" - private const val INITIAL_AMOUNT = "" - private const val UPDATE_DELAY = 10000L - private const val DEBOUNCE_AMOUNT_DELAY = 1000L - private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L + private companion object { + const val loggingTag = "SwapViewModel" + const val INITIAL_AMOUNT = "" + const val UPDATE_DELAY = 10000L + const val DEBOUNCE_AMOUNT_DELAY = 1000L + const val UPDATE_BALANCE_DELAY_MILLIS = 11000L } } \ No newline at end of file From 0977f12e801a923034101ed7fa405ccdb4d07708 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Mar 2024 13:35:47 +0500 Subject: [PATCH 017/109] Updated on 2026-08-14 --- .../presentation/state/SendNotification.kt | 15 ++---------- .../state/SendNotificationFactory.kt | 24 ------------------- .../presentation/state/SendStateFactory.kt | 14 ++++++++++- .../presentation/viewmodel/SendViewModel.kt | 12 +++------- 4 files changed, 18 insertions(+), 47 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index c9736b29ad..64817ff69e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -23,12 +23,12 @@ internal sealed class SendNotification(val config: NotificationConfig) { ), ) { - object TotalExceedsBalance : Error( + data object TotalExceedsBalance : Error( title = resourceReference(R.string.send_notification_exceed_balance_title), subtitle = resourceReference(R.string.send_notification_exceed_balance_text), ) - object InvalidAmount : Error( + data object InvalidAmount : Error( title = resourceReference(R.string.send_notification_invalid_amount_title), subtitle = resourceReference(R.string.send_notification_invalid_amount_text), ) @@ -94,17 +94,6 @@ internal sealed class SendNotification(val config: NotificationConfig) { subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), ) - data class NetworkCoverage( - val amountReducedBy: String, - val amountReduced: String, - ) : Warning( - title = resourceReference(id = R.string.send_network_fee_warning_title), - subtitle = resourceReference( - id = R.string.send_network_fee_warning_content, - formatArgs = wrappedList(amountReducedBy, amountReduced), - ), - ) - data object FeeTooLow : Warning( title = resourceReference(id = R.string.send_notification_transaction_delay_title), subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt index 20ad57bca1..426ddab71a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt @@ -52,7 +52,6 @@ internal class SendNotificationFactory( addDustWarningNotification(feeAmount, sendAmount) addTransactionLimitErrorNotification(feeAmount, sendAmount) // warnings - addFeeCoverageNotification(sendState.isSubtract, sendAmount) addExistentialWarningNotification(feeAmount, sendAmount) addHighFeeWarningNotification(sendAmount, sendState.ignoreAmountReduce) addTooLowNotification(feeState) @@ -257,29 +256,6 @@ internal class SendNotificationFactory( } } - private fun MutableList.addFeeCoverageNotification( - isSubtract: Boolean, - amountValue: BigDecimal, - ) { - val state = currentStateProvider() - val cryptoCurrency = cryptoCurrencyStatusProvider().currency - val feeAmount = state.feeState?.fee?.amount?.value ?: BigDecimal.ZERO - - val amountReducedValue = amountValue.minus(feeAmount) - val amountReducedByValue = amountValue.minus(amountReducedValue) - val amountReducedBy = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = amountReducedByValue, - cryptoCurrency = cryptoCurrency, - ) - val amountReduced = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = amountReducedValue, - cryptoCurrency = cryptoCurrency, - ) - if (isSubtract) { - add(SendNotification.Warning.NetworkCoverage(amountReducedBy, amountReduced)) - } - } - private fun MutableList.addTooLowNotification(feeState: SendStates.FeeState) { val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 53f1815f01..0b45a8adda 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryItem @@ -215,9 +216,20 @@ internal class SendStateFactory( //endregion //region send - fun onSubtractSelect(isSubtract: Boolean): SendUiState { + fun onSubtractSelect(isSubtract: Boolean, isAmountSubtractAvailable: Boolean): SendUiState { val state = currentStateProvider() + val fee = state.feeState?.fee ?: return state + val amountState = state.amountState ?: return state + val amount = amountState.amountTextField.cryptoAmount + val amountValue = amount.value ?: return state + val amountToSend = if (isSubtract && isAmountSubtractAvailable) { + val feeValue = fee.amount.value ?: return state + amountValue.minus(feeValue) + } else { + amountValue + } return state.copy( + amountState = amountStateConverter.convert(amountToSend.parseBigDecimal(amount.decimals)), sendState = state.sendState.copy(isSubtract = isSubtract), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 30b3e00367..93d739cd4b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -430,7 +430,7 @@ internal class SendViewModel @Inject constructor( ) return } else { - uiState = stateFactory.onSubtractSelect(false) + uiState = stateFactory.onSubtractSelect(false, isAmountSubtractAvailable) analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false)) } if (checkIfFeeTooLow(uiState)) { @@ -569,7 +569,7 @@ internal class SendViewModel @Inject constructor( } override fun onSubtractSelect() { - uiState = stateFactory.onSubtractSelect(true) + uiState = stateFactory.onSubtractSelect(true, isAmountSubtractAvailable) stateRouter.showSend() analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(true)) } @@ -676,16 +676,10 @@ internal class SendViewModel @Inject constructor( val fee = feeState.fee ?: return val memo = uiState.recipientState?.memoTextField?.value val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return - val amountToSend = if (uiState.sendState.isSubtract && isAmountSubtractAvailable) { - val feeValue = fee.amount.value ?: return - amountValue.minus(feeValue) - } else { - amountValue - } viewModelScope.launch(dispatchers.main) { createTransactionUseCase( - amount = amountToSend.convertToAmount(cryptoCurrency), + amount = amountValue.convertToAmount(cryptoCurrency), fee = fee, memo = memo, destination = recipient, From 79930c10220fa08a2cc920ad72740c0eb013b40d Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 9 Mar 2024 11:28:28 +0800 Subject: [PATCH 018/109] Updated on 2026-08-14 --- .../tap/di/domain/FeedbackDomainModule.kt | 25 +++++ domain/feedback/build.gradle.kts | 3 + .../GetSupportFeedbackEmailUseCase.kt | 95 +++++++++++++++++++ .../feedback/models/SupportFeedbackEmail.kt | 10 ++ 4 files changed, 133 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt new file mode 100644 index 0000000000..f2aea82f57 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt @@ -0,0 +1,25 @@ +package com.tangem.tap.di.domain + +import android.content.Context +import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase +import com.tangem.domain.feedback.repository.FeedbackRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object FeedbackDomainModule { + + @Provides + @Singleton + fun provideGetFeedbackToSupportUseCase( + feedbackRepository: FeedbackRepository, + @ApplicationContext context: Context, + ): GetSupportFeedbackEmailUseCase { + return GetSupportFeedbackEmailUseCase(feedbackRepository = feedbackRepository, resources = context.resources) + } +} \ No newline at end of file diff --git a/domain/feedback/build.gradle.kts b/domain/feedback/build.gradle.kts index 83a35acde4..3e7859b754 100644 --- a/domain/feedback/build.gradle.kts +++ b/domain/feedback/build.gradle.kts @@ -9,5 +9,8 @@ android { } dependencies { + implementation(deps.arrow.core) + implementation(deps.jodatime) + implementation(projects.core.res) } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt new file mode 100644 index 0000000000..150c5c0b2c --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt @@ -0,0 +1,95 @@ +package com.tangem.domain.feedback + +import android.content.res.Resources +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.SupportFeedbackEmail +import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.feedback.utils.skipLine +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormatterBuilder +import java.util.Locale + +/** + * Get email with feedback for support + * + * @property feedbackRepository feedback repository + * @property resources resources for getting strings + * +[REDACTED_AUTHOR] + */ +class GetSupportFeedbackEmailUseCase( + private val feedbackRepository: FeedbackRepository, + private val resources: Resources, +) { + + // 00.00 00:00:00.000 + private val dateFormatter = DateTimeFormatterBuilder() + .appendDayOfMonth(2) + .appendLiteral('.') + .appendMonthOfYear(2) + .appendLiteral(' ') + .appendHourOfDay(2) + .appendLiteral(':') + .appendMinuteOfHour(2) + .appendLiteral(':') + .appendSecondOfMinute(2) + .appendLiteral('.') + .appendMillisOfSecond(3) + .toFormatter() + .withLocale(Locale.getDefault()) + + suspend operator fun invoke(): SupportFeedbackEmail { + val cardInfo = feedbackRepository.getCardInfo() + + return SupportFeedbackEmail( + address = getEmail(isStart2Coin = cardInfo.isStart2Coin), + subject = getSubject(isStart2Coin = cardInfo.isStart2Coin), + message = createMessage(cardInfo), + file = feedbackRepository.createLogFile(logs = getLogs()), + ) + } + + private fun getEmail(isStart2Coin: Boolean): String { + return if (isStart2Coin) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL + } + + private fun getSubject(isStart2Coin: Boolean): String { + return resources.getString( + if (isStart2Coin) { + R.string.feedback_subject_support + } else { + R.string.feedback_subject_support_tangem + }, + ) + } + + private suspend fun createMessage(cardInfo: CardInfo): String { + return StringBuilder().apply { + append(resources.getString(R.string.feedback_preface_support)) + skipLine() + append(resources.getString(R.string.feedback_data_collection_message)) + skipLine() + append( + FeedbackDataBuilder().apply { + addCardInfo(cardInfo) + addDelimiter() + addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList()) + addDelimiter() + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + }.build(), + ) + }.toString() + } + + private suspend fun getLogs(): List { + return feedbackRepository.getAppLogs().map { + val date = dateFormatter.print(DateTime(it.timestamp)) + "$date: ${it.message}\n" + } + } + + private companion object { + const val START2COIN_SUPPORT_EMAIL = "cardsupport@start2coin.com" + const val TANGEM_SUPPORT_EMAIL = "support@tangem.com" + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt new file mode 100644 index 0000000000..35faf18f6f --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.feedback.models + +import java.io.File + +data class SupportFeedbackEmail( + val address: String, + val subject: String, + val message: String, + val file: File? = null, +) \ No newline at end of file From 2d6b318b307e2c17e5fa035929ffadfd248713f1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Mar 2024 15:45:38 +0100 Subject: [PATCH 019/109] Updated on 2026-08-14 --- .../models/AddCustomTokenViewsModels.kt | 6 ++-- core/res/src/main/res/values-ru/strings.xml | 29 ++---------------- .../src/main/res/values-zh-rTW/strings.xml | 17 ----------- core/res/src/main/res/values/strings.xml | 30 ++----------------- .../wallet2/ui/ImportSeedPhraseScreen.kt | 26 +++++++++++++++- .../feature/swap/domain/models/DataError.kt | 2 +- .../tangem/feature/swap/ui/StateBuilder.kt | 6 ++-- .../components/exchange/ExchangeProvider.kt | 3 +- .../state/model/WalletBottomSheetConfig.kt | 5 +++- 9 files changed, 42 insertions(+), 82 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt index 16fba8e3f2..6ba979beab 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt @@ -291,12 +291,12 @@ enum class DerivationPathSelectorType { internal sealed class AddCustomTokenWarning(val description: TextReference) { /** Potential scam warning */ - object PotentialScamToken : AddCustomTokenWarning( + data object PotentialScamToken : AddCustomTokenWarning( description = TextReference.Res(R.string.custom_token_validation_error_not_found), ) /** Token already added warning */ - object TokenAlreadyAdded : AddCustomTokenWarning( + data object TokenAlreadyAdded : AddCustomTokenWarning( description = TextReference.Res(R.string.custom_token_validation_error_already_added), ) @@ -305,7 +305,7 @@ internal sealed class AddCustomTokenWarning(val description: TextReference) { description = TextReference.Res(R.string.alert_manage_tokens_unsupported_message, networkName), ) - object WrongDerivationPath : AddCustomTokenWarning( + data object WrongDerivationPath : AddCustomTokenWarning( description = TextReference.Res(R.string.custom_token_invalid_derivation_path), ) } diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 92eb872a29..562af4dcb0 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -7,7 +7,6 @@ Отправлено успешно Обратиться в поддержку Эта функция недоступна в демонстрационном режиме - Не удалось отправить письмо Причина: %s Не могу отправить транзакцию Выбранный кошелёк не поддерживает сеть %1$s @@ -46,7 +45,6 @@ Отключите эту опцию, если не хотите, чтобы эта карта использовалась для сброса кодов доступа на другие карты этого кошелька. Обратите внимание, сброс кода также не будет доступен на этой карте. Использовать эту карту для сброса кода доступа на других картах в этом кошельке - Отключить возможность сброса кода доступа на этой карте или других картах этого кошелька Восстановление кода доступа Сбросить Вы уверены, что хотите это сделать? @@ -102,7 +100,6 @@ Нет адреса OK Основная карта - Вставить Подробнее Получить Отклонить @@ -119,7 +116,6 @@ Поделиться Подписать Подписать и отправить - Стейкинг Начать Отправить Успешно @@ -209,13 +205,11 @@ Ожидание депозита Ожидаем пополнения... Возвращено - Возврат... Отправляем Отправка средств... Отправлено Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий. Статус обмена - Верифицировано Требуется верификация Список токенов в вашем кошельке Получение наилучших курсов... @@ -248,9 +242,7 @@ Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции Чтобы создать кошелек, приложите карту как показано выше и не убирайте до окончания операции - Чтобы сбросить настройки до заводских, приложите карту как показано выше и не убирайте до окончания операции Приложите, чтобы отсканировать - Чтобы подписать транзакцию, приложите карту как показано выше и не убирайте до окончания операции Приложите, чтобы подписать Приложите карту Вы обновили данные биометрии, отсканируйте свою карту для входа @@ -438,7 +430,6 @@ Уже содержится в введенном адресе Вычесть Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму? - Комиссия увеличилась Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? Причина: %1$s\nКод: %2$s Транзакция не выполнена @@ -471,10 +462,10 @@ Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Установлена высокая комиссия Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s. - Увеличение комиссии + Комиссия увеличилась Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению Недопустимая сумма - Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s. + Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %2$s. Адрес получателя не активирован. \nПожалуйста, измените сумму отправки, чтобы продолжить. Сумма отправки не может быть менее %s Уменьшить на %s @@ -500,16 +491,11 @@ Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Революционный аппаратный кошелек До трех карт с одним кошельком - До - трех карт - с одним кошельком Все ключи в безопасности Аппаратный кошелек для ваших биткоинов, эфира и многих других валют одновременно — все в одной карте Тысячи криптовалют Используйте его на ходу, в любом месте, в любое время. Без проводов и батареек. Как только понадобится крипта, просто приложите карту к телефону. Кошелек для каждого - Пройдите 3 урока, получите скидку на покупку Tangem Wallet и 1INCH токены - Пройти обучение Встречайте Tangem Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах Поддержка Web 3.0 @@ -577,14 +563,12 @@ Переименование кошелька Разблокировать все Разблокировать все с %s - История транзакций Блокчейн недоступен. Попробуйте позже. Отсканируйте карту Запрос на подпись сообщения.\n\n%s Dapp %1$s, запрос на\nподпись транзакции с BNB.\n\n%2$s Торговый ордер на %1$s\nЦена: %2$s\nСумма к получению: %3$s\nСумма к оплате: %4$s Детали транзакции:\nОт: %1$s\nК: %2$s\nСумма: %3$s - Транзакция с BNB успешно подписана и отправлена ​​в Dapp. Буфер обмена содержит код WalletConnect. Использовать скопированное значение или отсканировать QR-код Запрос на создание транзакции для %1$s\n%2$s\n\nСумма: %3$s\nКомиссия: %4$s\nВсего: %5$s\nБаланс: %6$s Невозможно отправить транзакцию. Недостаточно средств. @@ -598,8 +582,6 @@ Неверная карта выбрана в приложении Tangem Не удалось создать транзакцию из данных Dapp. Код: %s Произошла непредвиденная ошибка. Код ошибки: %d Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. - Сообщение было успешно подписано и отправлено в Dapp - Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново. Нет открытых сессий WalletConnect Упс. Нет сессий. Не удалось создать пару WalletConnect: %1$s @@ -611,14 +593,9 @@ Эту карту нельзя использовать с WalletConnect. Сеть не поддерживается. Пожалуйста, выберите другую сеть. Выберите сеть - Dapp не предоставил необходимые данные для открытия сессии WalletConnect - Не удалось найти сессию для обработки запроса Сессии WalletConnect Подключение к dApps WalletConnect - Транзакция успешно подписана и отправлена ​​в Dapp - Транзакция успешно подписана и отправлена ​​в блокчейн - Не удалось найти хэш транзакции Рыночная цена %s за 24 часа Сеть %s @@ -629,7 +606,6 @@ Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян. Ошибка активации Можно лучше - Узнать больше Нравится Понятно! Очень круто! @@ -672,7 +648,6 @@ Резервная копия отсутствует Эта карта ранее использовалась для подписи транзакций. Если она получена от ненадежного источника, рассмотрите возможность вывода своих средств. Если это ваша карта, дополнительных действий не требуется. Карта уже подписывала транзакции - Устройство несовместимо Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше Нравится Tangem? Необходима плата за аренду сети diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 3b90b9b331..da5557d604 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -7,7 +7,6 @@ 成功送出 請求支持 此功能不在展示模式中提供 - 發送電子郵件失敗 原因:%s 無法發送交易 此卡不支持%1$s網路上的代幣因為韌體限制 @@ -30,7 +29,6 @@ 如果您不希望使用此卡重置此錢包中其他卡上的訪問密碼,請禁用此選項。請注意,這也會阻止您重置此卡上的訪問密碼。 允許您使用此卡重置此錢包中其他卡上的訪問密碼 - 禁用重置此卡或此錢包中其他卡上的訪問密碼的功能 恢復訪問密碼 重置 您確定要這麼做嗎? @@ -137,9 +135,7 @@ 要更改訪問密碼,請完全按照上圖所示連接手機和卡片 要更改密碼,請完全按照上圖所示連接手機和卡 要創建錢包,請完全按照上圖所示連接手機和卡 - 要重置為出廠設置,請完全按照上圖所示連接手機和卡片 點擊掃描 - 要簽名,請完全按照上圖所示連接手機和卡 點擊簽名 點按卡片 您已更新生物識別登入,掃描您的卡進入 @@ -278,9 +274,6 @@ 安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中 創新式的硬體錢包 最多 3張實體卡片 到一個錢包 - 最多 - 3張實體卡片 - 到一個錢包 更多的安全備份 一個冷錢包,可同時將您的比特幣、以太幣和其他幣種-全部存在一張卡中 千種虛擬貨幣 @@ -328,14 +321,12 @@ 錢包名稱 重新命名錢包 用 %s 解鎖全部 - 交易記錄 區塊鍊無法使用。稍後再試 掃描卡片 請求籤署消息。%s Dapp %1$s,請求\n簽署 BNB 交易。\n%2$s %1$s 的交易訂單\n價格: %2$s\n接收金額:%3$s\n支付數量: %4$s 交易明細:\n從: %1$s\n到: %2$s\n數量: %3$s - BNB 交易已成功簽署並發送至 Dapp 剪貼板包含 WalletConnect 代碼。使用複制的值或掃描二維碼 請求為 %1$s 創建交易\n%2$s\n\n數量: %3$s\n費用: %4$s\n全部的: %5$s\n餘額: %6$s 無法交易,無足夠資金 @@ -349,8 +340,6 @@ 在 Tangem App 中選擇了錯誤的卡 無法從 Dapp 數據創建交易。代碼: %s 我們遇到了未知錯誤。錯誤代碼:%d。如果問題仍然存在-請隨時聯繫我們的支持人員 - 消息已成功簽名並發送至Dapp - 沒有 %s 網路,請先加入後再試一次 沒有已連結的WalletConnect Ooops, 沒有連接 從剪貼板貼上 @@ -361,20 +350,14 @@ 此卡不能用於建立 WalletConnect 連接 不支持此網絡。請選擇其他網絡 選擇網路 - Dapp 沒有提供必要的數據來建立 WalletConnect 連接 - 找不到請求的連接 WalletConnect 連接 連結到Dapps WalletConnect - 交易已成功簽署並發送至 Dapp - 交易已成功簽署並發送至區塊鏈 - 未能找到交易哈希 %s市場價格 地址已復製到剪貼板 無網路 Tangem 可以更好 - 了解更多 了解 真的很酷! 此卡可能是生產樣本或偽造品 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 53492c3a02..4f8f87c686 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -7,7 +7,6 @@ Sent successfully Request support This feature is disabled in Demo mode - Failed to send the email Reason: %s Can\'t send a transaction The selected does not support the %1$s network @@ -45,7 +44,6 @@ Disable this option if you don\'t want this card to be used to reset access codes on other cards in this wallet. Please note that this will also prevent you from resetting the access code on this card. Allows you to use this card to reset access code on other cards in this wallet - Disable the ability to reset the access code on this card or other cards in this wallet Access code recovery Reset Are you sure you want to do this? @@ -93,7 +91,6 @@ Get addresses Go to provider Import - Learn & Earn Locked Main network Network fee @@ -102,7 +99,6 @@ No address OK Primary Card - Paste Read more Receive Reject @@ -119,7 +115,6 @@ Share Sign Sign and send - Stake Start Submit Success @@ -209,13 +204,11 @@ Awaiting deposit Awaiting deposit... Refunded - Refunding... Sending to you Sending to you... Sent Provider-sourced data. Estimated amount subject to change due to market conditions. Exchange status - Verified Verification required List of all tokens added to your wallet Fetching best rates... @@ -249,9 +242,7 @@ To change the access code tap the card as shown above and do not remove until the end of the operation To change the passcode tap the card as shown above and do not remove until the end of the operation To create the wallet tap the card as shown above and do not remove until the end of the operation - To reset to factory settings tap the card as shown above and do not remove until the end of the operation Tap to scan - To sign tap the card as shown above and do not remove until the end of the operation Tap to sign Tap the card You have updated biometrics, scan your card to enter @@ -435,7 +426,6 @@ Already included in the entered address Subtract Not enough funds to cover the network commission. Subtract the commission from the amount sent? - Fee is increased You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue? Reason: %1$s\nCode: %2$s The transaction is not completed @@ -478,7 +468,7 @@ Fee is increased The included commission exceeds the transfer amount, leading to a negative value Invalid amount - The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s. + The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %2$s. Target account is not created. Please change the amount to send. The amount to send must be at least %s Reduce by %s @@ -509,16 +499,11 @@ Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet Up to 3 physical cards to one wallet - Up to - 3 physical cards - to one wallet Ultra Secure Backup A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card Thousands of Currencies Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. The Wallet for Everyone - Take three lessons, get a discount on your Tangem Wallet, and receive 1INCH tokens to your wallet - Learn Meet Tangem Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services Web 3.0 Compatible @@ -586,14 +571,12 @@ Rename Wallet Unlock all Unlock all with %s - Transaction history Blockchain is unreachable. Try later Scan the card Requesting to sign a message.\n\n%s Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s Trade order for %1$s\nPrice: %2$s\nAmount to receive: %3$s\nAmount to pay: %4$s Transaction details:\nFrom: %1$s\nTo: %2$s\nAmount: %3$s - The BNB transaction has been successfully signed and sent to the Dapp Clipboard contain WalletConnect code. Use copied value or scan QR-code Request to create transaction for %1$s\n%2$s\n\nAmount: %3$s\nFee: %4$s\nTotal: %5$s\nBalance: %6$s Can\'t send transaction. Not enough funds. @@ -607,8 +590,6 @@ Wrong card selected in Tangem App Failed to create transaction from Dapp data. Code: %s We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support - The message has been successfully signed and sent to the Dapp - %s network not found. Please, add it first and try again. No opened WalletConnect sessions Ooops. No Sessions. Failed to pairing WalletConnect session: %1$s @@ -620,14 +601,9 @@ This card can\'t be used to establish WalletConnect session This network is not supported. Please select another network. Select network - Dapp didn\'t provide essential data to establish WalletConnect session - Failed to find session for request WalletConnect Sessions Connect to dApps WalletConnect - The transaction has been successfully signed and sent to the Dapp - The transaction has been succesfully signed and sent to the blockchain - Failed to find transaction hash %s Market Price last 24h %s network @@ -638,7 +614,6 @@ Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost. Backup error Could be better - Learn more Like it Ok, Got it! Really cool! @@ -653,7 +628,7 @@ You have active transaction Swap approval is underway and will be completed shortly Approval in progress - The minimum swapping amount is %1$s. Please ensure that the remaining balance after sending will not be less than %2$s. + The minimum swapping amount is %1$s. Please ensure that the remaining balance after the swap will not be less than %2$s. You do not have any %s exchangeable coins in your list No available tokens to swap To make a transaction you need to deposit some %1$s %2$s @@ -679,7 +654,6 @@ Missing backup This card has been previously used for transactions. If received from an untrusted source, consider withdrawing all funds. If it\'s your card, no action is required. Card has already signed transactions - Device incompatibility detected Your review keeps us motivated to make Tangem Wallet even better Enjoying Tangem? Network rent fee required diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt index 10248b767a..e0a5b674ad 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt @@ -22,7 +22,9 @@ import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.TangemTextFieldsDefault import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.R +import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseState +import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock @@ -67,7 +69,7 @@ fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Mo PrimaryButtonIconEnd( modifier = Modifier .fillMaxWidth(), - text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet), + text = stringResource(id = R.string.common_import), iconResId = R.drawable.ic_tangem_24, enabled = state.buttonCreateWallet.enabled, showProgress = state.buttonCreateWallet.showProgress, @@ -196,6 +198,28 @@ private fun SuggestionsBlockPreview_Dark( } } +@Preview +@Composable +private fun ImportSeedPhraseScreenPreview_Light( + @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, +) { + TangemTheme(isDark = false) { + ImportSeedPhraseScreen( + ImportSeedPhraseState( + tvSeedPhrase = TextFieldState(onTextFieldValueChanged = {}), + onSuggestedPhraseClick = {}, + buttonCreateWallet = ButtonState( + enabled = true, + isClickable = true, + showProgress = false, + onClick = {}, + ), + suggestionsList = suggestions, + ), + ) + } +} + private class SuggestionsPreviewParamsProvider : CollectionPreviewParameterProvider>( collection = listOf( persistentListOf( diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt index 21c40c5588..d4881c27b8 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt @@ -40,7 +40,7 @@ sealed class DataError { data class InvalidPayoutAddressError(override val code: Int = 992) : DataError() - object UnknownError : DataError() { + data object UnknownError : DataError() { override val code: Int = -1 } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 2587562721..8d89cd4924 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -385,7 +385,7 @@ internal class StateBuilder( NotificationConfig( title = resourceReference(R.string.send_notification_invalid_amount_title), subtitle = resourceReference( - R.string.send_notification_invalid_minimum_amount_text, + R.string.warning_express_dust_message, wrappedList( it.dustValue.toPlainString(), it.dustValue.toPlainString(), @@ -677,7 +677,7 @@ internal class StateBuilder( subtitle = if (dataError is DataError.UnknownError) { resourceReference(R.string.common_unknown_error) } else { - resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())) + resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString())) }, iconResId = R.drawable.img_attention_20, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( @@ -862,7 +862,7 @@ internal class StateBuilder( SwapWarning.GeneralWarning( notificationConfig = NotificationConfig( title = TextReference.Res(R.string.warning_express_refresh_required_title), - subtitle = TextReference.Res(R.string.generic_error_code, wrappedList(code)), + subtitle = TextReference.Res(R.string.express_error_code, wrappedList(code)), iconResId = R.drawable.ic_alert_triangle_20, buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( text = TextReference.Res(R.string.warning_button_refresh), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt index c08898d2dc..41278b195c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.inputrow.InputRowBestRate import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme @Composable @@ -67,7 +68,7 @@ internal fun ExchangeProvider( ) Text( modifier = Modifier.align(Alignment.CenterVertically), - text = "ID: $providerTxId", + text = stringResource(R.string.express_transaction_id, wrappedList(providerTxId)), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt index d6ec5fcc93..adfede4a1c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt @@ -36,7 +36,10 @@ sealed class WalletBottomSheetConfig( ), iconResId = R.drawable.ic_locked_24, primaryButtonConfig = ButtonConfig( - text = resourceReference(id = R.string.user_wallet_list_unlock_all), + text = resourceReference( + id = R.string.user_wallet_list_unlock_all_with, + formatArgs = wrappedList(resourceReference(R.string.common_biometrics)), + ), onClick = onUnlockClick, ), secondaryButtonConfig = ButtonConfig( From 3d50215ff9d69008269edc4bacfa96d5b2736e09 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Mar 2024 21:23:40 +0500 Subject: [PATCH 020/109] Updated on 2026-08-14 --- .../presentation/viewmodel/SendViewModel.kt | 92 ++++++++++++------- 1 file changed, 61 insertions(+), 31 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 93d739cd4b..b4b108f577 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -12,9 +12,11 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.* +import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.repository.CurrencyChecksRepository @@ -66,6 +68,7 @@ internal class SendViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val getFeeUseCase: GetFeeUseCase, @@ -207,7 +210,13 @@ internal class SendViewModel @Inject constructor( ifRight = { wallet -> userWallet = wallet checkIfSubtractAvailable() - getCurrenciesStatusUpdates(wallet) + + val isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() + val isMultiCurrency = wallet.isMultiCurrency + getCurrenciesStatusUpdates( + isSingleWalletWithToken = isSingleWalletWithToken, + isMultiCurrency = isMultiCurrency, + ) }, ifLeft = { uiState = eventStateFactory.getGenericErrorState( @@ -230,34 +239,38 @@ internal class SendViewModel @Inject constructor( .saveIn(balanceHidingJobHolder) } - private fun getCurrenciesStatusUpdates(wallet: UserWallet) { - val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency - + private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) { if (cryptoCurrency is CryptoCurrency.Coin) { - getCurrencyStatusUpdates(isSingleWallet = isSingleWallet) - .onEach { currencyStatus -> - currencyStatus.onRight { - onDataLoaded( - currencyStatus = it, - coinCurrencyStatus = it, - feeCurrencyStatus = getFeeCurrencyStatusSync(it), - ) - } + getCurrencyStatusUpdates( + isSingleWalletWithToken = isSingleWalletWithToken, + isMultiCurrency = isMultiCurrency, + ).onEach { currencyStatus -> + currencyStatus.onRight { + onDataLoaded( + currencyStatus = it, + coinCurrencyStatus = it, + feeCurrencyStatus = getFeeCurrencyStatusSync(it, isMultiCurrency), + ) } + } .flowOn(dispatchers.main) .launchIn(viewModelScope) .saveIn(balanceJobHolder) } else { combine( - flow = getCoinCurrencyStatusUpdates(isSingleWallet = isSingleWallet), - flow2 = getCurrencyStatusUpdates(isSingleWallet = isSingleWallet), - ) { coinStatus, maybeCurrencyStatus -> - if (coinStatus.isRight() && maybeCurrencyStatus.isRight()) { + flow = getCoinCurrencyStatusUpdates(isSingleWalletWithToken), + flow2 = getCurrencyStatusUpdates( + isSingleWalletWithToken = isSingleWalletWithToken, + isMultiCurrency = isMultiCurrency, + ), + ) { maybeCoinStatus, maybeCurrencyStatus -> + if (maybeCoinStatus.isRight() && maybeCurrencyStatus.isRight()) { val currencyStatus = maybeCurrencyStatus.getOrElse { error("Currency status is unreachable") } + val coinStatus = maybeCoinStatus.getOrElse { error("Coin status is unreachable") } onDataLoaded( currencyStatus = currencyStatus, - coinCurrencyStatus = coinStatus.getOrElse { error("Coin status is unreachable") }, - feeCurrencyStatus = getFeeCurrencyStatusSync(currencyStatus), + coinCurrencyStatus = coinStatus, + feeCurrencyStatus = getFeeCurrencyStatusSync(currencyStatus, isMultiCurrency), ) } } @@ -267,24 +280,41 @@ internal class SendViewModel @Inject constructor( } } - private fun getCoinCurrencyStatusUpdates(isSingleWallet: Boolean) = getNetworkCoinStatusUseCase( + private fun getCoinCurrencyStatusUpdates(isSingleWalletWithToken: Boolean) = getNetworkCoinStatusUseCase( userWalletId = userWalletId, networkId = cryptoCurrency.network.id, derivationPath = cryptoCurrency.network.derivationPath, - isSingleWalletWithTokens = isSingleWallet, + isSingleWalletWithTokens = isSingleWalletWithToken, ).conflate().distinctUntilChanged() - private fun getCurrencyStatusUpdates(isSingleWallet: Boolean) = getCurrencyStatusUpdatesUseCase( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = isSingleWallet, - ).conflate().distinctUntilChanged() + private fun getCurrencyStatusUpdates( + isSingleWalletWithToken: Boolean, + isMultiCurrency: Boolean, + ): Flow> { + return if (isMultiCurrency) { + getCurrencyStatusUpdatesUseCase( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + isSingleWalletWithTokens = isSingleWalletWithToken, + ).conflate().distinctUntilChanged() + } else { + getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId) + } + } - private suspend fun getFeeCurrencyStatusSync(cryptoCurrencyStatus: CryptoCurrencyStatus) = - getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - ).getOrNull() ?: error("Fee currency is unreachable") + private suspend fun getFeeCurrencyStatusSync( + cryptoCurrencyStatus: CryptoCurrencyStatus, + isMultiCurrency: Boolean, + ): CryptoCurrencyStatus { + return if (isMultiCurrency) { + getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull() ?: error("Fee currency is unreachable") + } else { + cryptoCurrencyStatus + } + } private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() From a0834799ea9f6ffe9dc33b6bdd57ba2d5be0b38b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Mar 2024 17:42:41 +0800 Subject: [PATCH 021/109] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 2 +- .../middlewares/TradeCryptoMiddleware.kt | 1 + .../exchangeServices/moonpay/BlockchainExt.kt | 37 ++++++++ .../moonpay/MoonPayService.kt | 94 ++++++++++--------- .../models/MoonPayAvailableCurrency.kt | 7 ++ .../models/MoonPaySupportedCurrency.kt | 3 + .../deeplink/global/SellCurrencyDeepLink.kt | 4 +- .../domain/tokens/legacy/TradeCryptoAction.kt | 5 +- .../send/api/navigation/SendRouter.kt | 1 + .../presentation/state/SendStateFactory.kt | 8 +- .../SendRecipientMemoFieldConverter.kt | 26 +++-- .../recipient/SendRecipientStateConverter.kt | 10 +- .../presentation/viewmodel/SendViewModel.kt | 3 +- .../viewmodels/TokenDetailsViewModel.kt | 3 +- .../deeplink/WalletDeepLinksHandler.kt | 1 + 15 files changed, 141 insertions(+), 64 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/BlockchainExt.kt create mode 100644 app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPayAvailableCurrency.kt create mode 100644 app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPaySupportedCurrency.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4b57ad94ed..093b3eb247 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -130,7 +130,7 @@ diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 28a73436b5..a76ae2c37e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -318,6 +318,7 @@ object TradeCryptoMiddleware { SendRouter.TRANSACTION_ID_KEY to txInfo?.transactionId, SendRouter.DESTINATION_ADDRESS_KEY to txInfo?.destinationAddress, SendRouter.AMOUNT_KEY to txInfo?.amount, + SendRouter.TAG_KEY to txInfo?.tag, ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/BlockchainExt.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/BlockchainExt.kt new file mode 100644 index 0000000000..c5f04b9039 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/BlockchainExt.kt @@ -0,0 +1,37 @@ +package com.tangem.tap.network.exchangeServices.moonpay + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Blockchain.* +import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPaySupportedCurrency + +internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? + get() = when (this) { + Algorand -> MoonPaySupportedCurrency(networkCode = "algorand", currencyCode = "algo") + Aptos -> MoonPaySupportedCurrency(networkCode = "aptos", currencyCode = "apt") + Arbitrum -> MoonPaySupportedCurrency(networkCode = "arbitrum", currencyCode = "eth_arbitrum") + Avalanche -> MoonPaySupportedCurrency(networkCode = "avalanche_c_chain", currencyCode = "avax_cchain") + Binance -> MoonPaySupportedCurrency(networkCode = "bnb_chain", currencyCode = "bnb") + Bitcoin -> MoonPaySupportedCurrency(networkCode = "bitcoin", currencyCode = "btc") + BitcoinCash -> MoonPaySupportedCurrency(networkCode = "bitcoin_cash", currencyCode = "bch") + BSC -> MoonPaySupportedCurrency(networkCode = "binance_smart_chain", currencyCode = "bnb_bsc") + Cardano -> MoonPaySupportedCurrency(networkCode = "cardano", currencyCode = "ada") + Cosmos -> MoonPaySupportedCurrency(networkCode = "cosmos", currencyCode = "atom") + Dogecoin -> MoonPaySupportedCurrency(networkCode = "dogecoin", currencyCode = "doge") + Ethereum -> MoonPaySupportedCurrency(networkCode = "ethereum", currencyCode = "eth") + EthereumClassic -> MoonPaySupportedCurrency(networkCode = "ethereum_classic", currencyCode = "etc") + Hedera -> MoonPaySupportedCurrency(networkCode = "hedera", currencyCode = "hbar") + Litecoin -> MoonPaySupportedCurrency(networkCode = "litecoin", currencyCode = "ltc") + Near -> MoonPaySupportedCurrency(networkCode = "near", currencyCode = "near") + Optimism -> MoonPaySupportedCurrency(networkCode = "optimism", currencyCode = "eth_optimism") + Polkadot -> MoonPaySupportedCurrency(networkCode = "polkadot", currencyCode = "dot") + Polygon -> MoonPaySupportedCurrency(networkCode = "polygon", currencyCode = "matic_polygon") + Ravencoin -> MoonPaySupportedCurrency(networkCode = "ravencoin", currencyCode = "rvn") + Solana -> MoonPaySupportedCurrency(networkCode = "solana", currencyCode = "sol") + Stellar -> MoonPaySupportedCurrency(networkCode = "stellar", currencyCode = "xlm") + Tezos -> MoonPaySupportedCurrency(networkCode = "tezos", currencyCode = "xtz") + TON -> MoonPaySupportedCurrency(networkCode = "ton", currencyCode = "ton") + Tron -> MoonPaySupportedCurrency(networkCode = "tron", currencyCode = "trx") + VeChain -> MoonPaySupportedCurrency(networkCode = "vechain", currencyCode = "vet") + XRP -> MoonPaySupportedCurrency(networkCode = "ripple", currencyCode = "xrp") + else -> null + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 947e8f1b69..3e5a70b2a2 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -12,6 +12,7 @@ import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME +import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec @@ -35,42 +36,46 @@ class MoonPayService( override suspend fun update() { withIOContext { performRequest { - val userStatusResult = performRequest { api.getUserStatus(apiKey) } - if (userStatusResult is Result.Failure) return@performRequest - - val currenciesResult = performRequest { api.getCurrencies(apiKey) } - if (currenciesResult is Result.Failure) return@performRequest - - val userStatus = (userStatusResult as Result.Success).data - val currencies = (currenciesResult as Result.Success).data - -// val currenciesToBuy = mutableListOf() - val currenciesToSell = mutableListOf() - - currencies.forEach { currencyStatus -> - if (currencyStatus.type != "crypto" || currencyStatus.isSuspended || - !currencyStatus.supportsLiveMode - ) { - return@forEach - } - - if (userStatus.countryCode == "USA") { - if (!currencyStatus.isSupportedInUS) return@forEach - if (currencyStatus.notAllowedUSStates.contains(userStatus.stateCode)) return@forEach - } - val currencyCode = currencyStatus.code.uppercase() -// currenciesToBuy.add(currencyCode) - - if (currencyStatus.isSellSupported) currenciesToSell.add(currencyCode) + val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) { + is Result.Failure -> return@performRequest + is Result.Success -> result.data } -// currenciesToBuy.sort() - currenciesToSell.sort() + + val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) { + is Result.Failure -> return@performRequest + is Result.Success -> result.data + } + + val currenciesToSell = currencies + .filter { currency -> + checkGeneralRequirements(currency) && checkUSARequirements(userStatus, currency) + } + .mapNotNull { currency -> + MoonPayAvailableCurrency( + currencyCode = currency.code, + networkCode = currency.metadata?.networkCode ?: return@mapNotNull null, + contractAddress = currency.metadata.contractAddress, + ) + } status = MoonPayStatus(currenciesToSell, userStatus, currencies) } } } + private fun checkGeneralRequirements(currency: MoonPayCurrencies): Boolean { + return currency.type == "crypto" && !currency.isSuspended && currency.supportsLiveMode && + currency.isSellSupported + } + + private fun checkUSARequirements(userStatus: MoonPayUserStatus, currency: MoonPayCurrencies): Boolean { + return if (userStatus.countryCode == "USA") { + currency.isSupportedInUS && !currency.notAllowedUSStates.contains(userStatus.stateCode) + } else { + true + } + } + override fun isBuyAllowed(): Boolean = false override fun isSellAllowed(): Boolean { @@ -80,21 +85,21 @@ class MoonPayService( override fun availableForBuy(currency: Currency): Boolean = false override fun availableForSell(currency: Currency): Boolean { - val availableForSell = status?.availableForSell ?: return false - val metadata = status?.responseCurrencies?.filter { it.isSellSupported }?.map { it.metadata } if (!isSellAllowed()) return false - return when (currency) { - is Currency.Blockchain -> { - val blockchain = currency.blockchain - when { - blockchain.isTestnet() -> false - blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC -> false - else -> availableForSell.contains(currency.currencySymbol) + val availableForSell = status?.availableForSell ?: return false + + val supportedCurrency = currency.blockchain.moonPaySupportedCurrency ?: return false + return availableForSell.any { + when (currency) { + is Currency.Blockchain -> { + it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) && + it.currencyCode.equals(other = supportedCurrency.currencyCode, ignoreCase = true) + } + is Currency.Token -> { + it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) && + it.contractAddress.equals(other = currency.token.contractAddress, ignoreCase = true) } - } - is Currency.Token -> { - metadata?.any { it?.contractAddress.equals(currency.token.contractAddress, ignoreCase = true) } ?: false } } } @@ -103,7 +108,7 @@ class MoonPayService( action: CurrencyExchangeManager.Action, blockchain: Blockchain, cryptoCurrencyName: CryptoCurrencyName, - fatCurrency: String, + fiatCurrencyName: String, walletAddress: String, isDarkTheme: Boolean, ): String { @@ -115,7 +120,8 @@ class MoonPayService( .appendQueryParameter("apiKey", apiKey) .appendQueryParameter("baseCurrencyCode", cryptoCurrencyName) .appendQueryParameter("refundWalletAddress", walletAddress) - .appendQueryParameter("redirectURL", "tangem://sell-request.tangem.com") + .appendQueryParameter("redirectURL", "tangem://redirect_sell") + if (isDarkTheme) uri.appendQueryParameter("theme", "dark") val originalQuery = uri.build().encodedQuery ?: uri.build().toString() @@ -147,7 +153,7 @@ class MoonPayService( } private data class MoonPayStatus( - val availableForSell: List, + val availableForSell: List, val responseUserStatus: MoonPayUserStatus, val responseCurrencies: List, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPayAvailableCurrency.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPayAvailableCurrency.kt new file mode 100644 index 0000000000..5f7845f0c9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPayAvailableCurrency.kt @@ -0,0 +1,7 @@ +package com.tangem.tap.network.exchangeServices.moonpay.models + +internal data class MoonPayAvailableCurrency( + val currencyCode: String, + val networkCode: String, + val contractAddress: String?, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPaySupportedCurrency.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPaySupportedCurrency.kt new file mode 100644 index 0000000000..f858942b46 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPaySupportedCurrency.kt @@ -0,0 +1,3 @@ +package com.tangem.tap.network.exchangeServices.moonpay.models + +internal data class MoonPaySupportedCurrency(val networkCode: String, val currencyCode: String?) \ No newline at end of file diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt index 5d9047a83b..579c72689a 100644 --- a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt +++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt @@ -4,13 +4,14 @@ import com.tangem.core.deeplink.DeepLink class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink { - override val uri: String = "tangem://sell-request.tangem.com" + override val uri: String = "tangem://redirect_sell" override fun onReceive(params: Map) { val data = Data( transactionId = params["transactionId"] ?: return, baseCurrencyAmount = params["baseCurrencyAmount"] ?: return, depositWalletAddress = params["depositWalletAddress"] ?: return, + depositWalletAddressTag = params["depositWalletAddressTag"], ) onReceive(data) @@ -20,5 +21,6 @@ class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink { val transactionId: String, val baseCurrencyAmount: String, val depositWalletAddress: String, + val depositWalletAddressTag: String?, ) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index 1657a3ea05..092ad6d3ba 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -41,8 +41,9 @@ sealed class TradeCryptoAction : Action { data class Swap(val cryptoCurrency: CryptoCurrency) : TradeCryptoAction() data class TransactionInfo( - val amount: String, - val destinationAddress: String, val transactionId: String, + val destinationAddress: String, + val amount: String, + val tag: String? = null, ) } \ No newline at end of file diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt index fce7f71d36..4a2940f972 100644 --- a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt +++ b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt @@ -11,6 +11,7 @@ interface SendRouter { const val USER_WALLET_ID_KEY = "send_user_wallet_id" const val TRANSACTION_ID_KEY = "send_transaction_id" const val AMOUNT_KEY = "send_amount" + const val TAG_KEY = "send_tag" const val DESTINATION_ADDRESS_KEY = "send_destination_address" } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 0b45a8adda..3d28d8ae4a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -87,16 +87,18 @@ internal class SendStateFactory( val state = currentStateProvider() return state.copy( amountState = state.amountState ?: amountStateConverter.convert(""), - recipientState = state.recipientState ?: recipientStateConverter.convert(""), + recipientState = state.recipientState + ?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)), feeState = state.feeState ?: feeStateConverter.convert(Unit), ) } - fun getReadyState(amount: String, destinationAddress: String): SendUiState { + fun getReadyState(amount: String, destinationAddress: String, memo: String?): SendUiState { val state = currentStateProvider() return state.copy( amountState = state.amountState ?: amountStateConverter.convert(amount), - recipientState = state.recipientState ?: recipientStateConverter.convert(destinationAddress), + recipientState = state.recipientState + ?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)), feeState = state.feeState ?: feeStateConverter.convert(Unit), isEditingDisabled = true, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt index 1eeffb3762..a539d72859 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.state.recipient +import androidx.annotation.StringRes import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -15,13 +16,18 @@ import com.tangem.utils.converter.Converter internal class SendRecipientMemoFieldConverter( private val clickIntents: SendClickIntents, private val cryptoCurrencyStatus: Provider, -) : Converter { +) : Converter { - fun convertOrNull(): SendTextField.RecipientMemo? { + fun convertOrNull(memoValue: String?): SendTextField.RecipientMemo? { val cryptoCurrency = cryptoCurrencyStatus().currency + val memo = memoValue ?: "" return when (cryptoCurrency.network.id.value) { - Blockchain.XRP.id -> convert(R.string.send_destination_tag_field) + Blockchain.XRP.id -> { + convert( + value = Data(memo = memo, label = R.string.send_destination_tag_field), + ) + } Blockchain.Binance.id, Blockchain.TON.id, Blockchain.Cosmos.id, @@ -30,24 +36,30 @@ internal class SendRecipientMemoFieldConverter( Blockchain.Stellar.id, Blockchain.Hedera.id, Blockchain.Algorand.id, - -> convert(R.string.send_extras_hint_memo) + -> { + convert( + value = Data(memo = memo, label = R.string.send_extras_hint_memo), + ) + } else -> null } } - override fun convert(value: Int): SendTextField.RecipientMemo { + override fun convert(value: Data): SendTextField.RecipientMemo { return SendTextField.RecipientMemo( - value = "", + value = value.memo, onValueChange = clickIntents::onRecipientMemoValueChange, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Done, keyboardType = KeyboardType.Text, ), placeholder = resourceReference(R.string.send_optional_field), - label = resourceReference(value), + label = resourceReference(value.label), error = resourceReference(R.string.send_memo_destination_tag_error), disabledText = resourceReference(R.string.send_additional_field_already_included), isEnabled = true, ) } + + data class Data(val memo: String, @StringRes val label: Int) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt index ae375c97e2..dd7db52c45 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt @@ -10,7 +10,7 @@ import kotlinx.collections.immutable.persistentListOf internal class SendRecipientStateConverter( private val clickIntents: SendClickIntents, private val cryptoCurrencyStatusProvider: Provider, -) : Converter { +) : Converter { private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) } private val memoFieldConverter by lazy { @@ -20,14 +20,16 @@ internal class SendRecipientStateConverter( ) } - override fun convert(value: String): SendStates.RecipientState { + override fun convert(value: Data): SendStates.RecipientState { return SendStates.RecipientState( - addressTextField = addressFieldConverter.convert(value), - memoTextField = memoFieldConverter.convertOrNull(), + addressTextField = addressFieldConverter.convert(value.address), + memoTextField = memoFieldConverter.convertOrNull(value.memo), network = cryptoCurrencyStatusProvider().currency.network.name, isPrimaryButtonEnabled = false, wallets = persistentListOf(), recent = persistentListOf(), ) } + + data class Data(val address: String, val memo: String? = null) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index b4b108f577..cf90cca81f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -99,6 +99,7 @@ internal class SendViewModel @Inject constructor( private val transactionId: String? = savedStateHandle[SendRouter.TRANSACTION_ID_KEY] private val amount: String? = savedStateHandle[SendRouter.AMOUNT_KEY] private val destinationAddress: String? = savedStateHandle[SendRouter.DESTINATION_ADDRESS_KEY] + private val memo: String? = savedStateHandle[SendRouter.TAG_KEY] private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() @@ -342,7 +343,7 @@ internal class SendViewModel @Inject constructor( stateRouter.showSend() } transactionId != null && amount != null && destinationAddress != null -> { - uiState = stateFactory.getReadyState(amount, destinationAddress) + uiState = stateFactory.getReadyState(amount, destinationAddress, memo) stateRouter.showFee() updateNotifications() } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index fbdbcf8273..d705013208 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -175,9 +175,10 @@ internal class TokenDetailsViewModel @Inject constructor( status = cryptoCurrencyStatus ?: return, transactionInfo = data.let { TransactionInfo( - amount = it.baseCurrencyAmount, transactionId = it.transactionId, destinationAddress = it.depositWalletAddress, + amount = it.baseCurrencyAmount, + tag = it.depositWalletAddressTag, ) }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt index 0248645642..59e2022b58 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt @@ -84,6 +84,7 @@ internal class WalletDeepLinksHandler @Inject constructor( amount = it.baseCurrencyAmount, destinationAddress = it.depositWalletAddress, transactionId = it.transactionId, + tag = it.depositWalletAddressTag, ) } From 263ee55122e014ba33d6afc4d076ed14e7c7c918 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Mar 2024 21:48:15 +0500 Subject: [PATCH 022/109] Updated on 2026-08-14 --- app/build.gradle.kts | 5 +++- .../tap/di/domain/QrScanningDomainModule.kt | 11 ++++++-- .../walletconnect/WalletConnectMiddleware.kt | 2 +- .../walletconnect/WalletConnectViewModel.kt | 4 +-- .../dialogs/ClipboardOrScanQrDialog.kt | 2 +- .../tap/features/send/ui/SendFragment.kt | 4 +-- data/qr-scanning/.gitignore | 1 + data/qr-scanning/build.gradle.kts | 22 +++++++++++++++ .../qrscanning/di/QrScanningDataModule.kt | 20 ++++++++++++++ .../DefaultQrScanningEventsRepository.kt | 9 ++++--- domain/qr-scanning/.gitignore | 1 + domain/qr-scanning/build.gradle.kts | 13 +++++++++ domain/qr-scanning/models/.gitignore | 1 + domain/qr-scanning/models/build.gradle.kts | 4 +++ .../domain/qrscanning/models}/SourceType.kt | 2 +- .../repository}/QrScanningEventsRepository.kt | 4 +-- .../usecases}/EmitQrScannedEventUseCase.kt | 8 +++--- .../usecases}/ListenToQrScanningUseCase.kt | 7 +++-- features/qr-scanning/impl/build.gradle.kts | 6 ++++- .../feature/qrscanning/di/QrScanningModule.kt | 27 ------------------- .../InitializeQrScanningStateTransformer.kt | 2 +- .../viewmodel/BaseQrScanningClickIntents.kt | 2 +- .../viewmodel/QrScanningClickIntents.kt | 2 +- .../viewmodel/QrScanningViewModel.kt | 2 +- features/send/impl/build.gradle.kts | 3 ++- .../send/impl/navigation/DefaultSendRouter.kt | 2 +- .../send/impl/presentation/SendFragment.kt | 4 +-- settings.gradle.kts | 3 +++ 28 files changed, 114 insertions(+), 59 deletions(-) create mode 100644 data/qr-scanning/.gitignore create mode 100644 data/qr-scanning/build.gradle.kts create mode 100644 data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt rename {features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo => data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository}/DefaultQrScanningEventsRepository.kt (63%) create mode 100644 domain/qr-scanning/.gitignore create mode 100644 domain/qr-scanning/build.gradle.kts create mode 100644 domain/qr-scanning/models/.gitignore create mode 100644 domain/qr-scanning/models/build.gradle.kts rename {features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning => domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models}/SourceType.kt (56%) rename {features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo => domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository}/QrScanningEventsRepository.kt (67%) rename {features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase => domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases}/EmitQrScannedEventUseCase.kt (65%) rename {features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase => domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases}/ListenToQrScanningUseCase.kt (69%) delete mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4543073e2a..3682efb9f5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -65,6 +65,8 @@ dependencies { implementation(projects.domain.visa) implementation(projects.domain.onboarding) implementation(projects.domain.feedback) + implementation(projects.domain.qrScanning) + implementation(projects.domain.qrScanning.models) implementation(projects.common) implementation(projects.core.analytics) @@ -95,6 +97,7 @@ dependencies { implementation(projects.data.promo) implementation(projects.data.onboarding) implementation(projects.data.feedback) + implementation(projects.data.qrScanning) /** Features */ implementation(projects.features.onboarding) @@ -112,9 +115,9 @@ dependencies { implementation(projects.features.wallet.impl) implementation(projects.features.tokendetails.api) implementation(projects.features.tokendetails.impl) - implementation(projects.features.send.api) implementation(projects.features.manageTokens.api) implementation(projects.features.manageTokens.impl) + implementation(projects.features.send.api) implementation(projects.features.send.impl) implementation(projects.features.qrScanning.api) implementation(projects.features.qrScanning.impl) diff --git a/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt index ba2c69965b..43f66670c6 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt @@ -1,7 +1,8 @@ package com.tangem.tap.di.domain -import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository -import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository +import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,4 +18,10 @@ internal object QrScanningDomainModule { fun provideListenToQrScanUseCase(repository: QrScanningEventsRepository): ListenToQrScanningUseCase { return ListenToQrScanningUseCase(repository) } + + @Provides + @Singleton + fun provideEmitQrScannedEventUseCase(repository: QrScanningEventsRepository): EmitQrScannedEventUseCase { + return EmitQrScannedEventUseCase(repository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index c6422a0706..c45ec9dae3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -13,12 +13,12 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.walletconnect.WalletConnectActions import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.feature.qrscanning.SourceType import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt index c096d0531e..cb6a206195 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt @@ -2,8 +2,8 @@ package com.tangem.tap.features.details.ui.walletconnect import androidx.lifecycle.* import arrow.core.getOrElse -import com.tangem.feature.qrscanning.SourceType -import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt index d3f92e4286..23a605e2d5 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt @@ -7,7 +7,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.feature.qrscanning.SourceType +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.store diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 775a5cc578..84628d0ffa 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -25,10 +25,10 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.feature.qrscanning.SourceType -import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase import com.tangem.sdk.extensions.hideSoftKeyboard import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.analytics.events.Token diff --git a/data/qr-scanning/.gitignore b/data/qr-scanning/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/qr-scanning/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/qr-scanning/build.gradle.kts b/data/qr-scanning/build.gradle.kts new file mode 100644 index 0000000000..ec2f620228 --- /dev/null +++ b/data/qr-scanning/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.data.qrscanning" +} + +dependencies { + + /** Domain */ + implementation(projects.domain.qrScanning) + implementation(projects.domain.qrScanning.models) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt new file mode 100644 index 0000000000..efe697f667 --- /dev/null +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt @@ -0,0 +1,20 @@ +package com.tangem.data.qrscanning.di + +import com.tangem.data.qrscanning.repository.DefaultQrScanningEventsRepository +import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object QrScanningDataModule { + + @Provides + @Singleton + fun provideQrScanningEventsRepository(): QrScanningEventsRepository { + return DefaultQrScanningEventsRepository() + } +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/DefaultQrScanningEventsRepository.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt similarity index 63% rename from features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/DefaultQrScanningEventsRepository.kt rename to data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt index 146bee2ef6..1732996a6b 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/DefaultQrScanningEventsRepository.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt @@ -1,7 +1,10 @@ -package com.tangem.feature.qrscanning.repo +package com.tangem.data.qrscanning.repository -import com.tangem.feature.qrscanning.SourceType -import kotlinx.coroutines.flow.* +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository { diff --git a/domain/qr-scanning/.gitignore b/domain/qr-scanning/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/qr-scanning/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/qr-scanning/build.gradle.kts b/domain/qr-scanning/build.gradle.kts new file mode 100644 index 0000000000..fe5d126eb1 --- /dev/null +++ b/domain/qr-scanning/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + + /** Domain */ + implementation(projects.domain.qrScanning.models) + + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) +} \ No newline at end of file diff --git a/domain/qr-scanning/models/.gitignore b/domain/qr-scanning/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/qr-scanning/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/qr-scanning/models/build.gradle.kts b/domain/qr-scanning/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/qr-scanning/models/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/SourceType.kt b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/SourceType.kt similarity index 56% rename from features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/SourceType.kt rename to domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/SourceType.kt index 96640d74e6..87be83fd0c 100644 --- a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/SourceType.kt +++ b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/SourceType.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.qrscanning +package com.tangem.domain.qrscanning.models enum class SourceType { WALLET_CONNECT, diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/QrScanningEventsRepository.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt similarity index 67% rename from features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/QrScanningEventsRepository.kt rename to domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt index 3640e79c2a..b834c4dbcd 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/QrScanningEventsRepository.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt @@ -1,6 +1,6 @@ -package com.tangem.feature.qrscanning.repo +package com.tangem.domain.qrscanning.repository -import com.tangem.feature.qrscanning.SourceType +import com.tangem.domain.qrscanning.models.SourceType import kotlinx.coroutines.flow.Flow interface QrScanningEventsRepository { diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/EmitQrScannedEventUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt similarity index 65% rename from features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/EmitQrScannedEventUseCase.kt rename to domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt index cc7f7bc6c2..ebe0e5980f 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/EmitQrScannedEventUseCase.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt @@ -1,12 +1,12 @@ -package com.tangem.feature.qrscanning.usecase +package com.tangem.domain.qrscanning.usecases import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.feature.qrscanning.SourceType -import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository -internal class EmitQrScannedEventUseCase( +class EmitQrScannedEventUseCase( private val repository: QrScanningEventsRepository, ) { suspend operator fun invoke(type: SourceType, qrCode: String): Either { diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/ListenToQrScanningUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt similarity index 69% rename from features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/ListenToQrScanningUseCase.kt rename to domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt index e3ef9f9dc1..3c42400420 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/ListenToQrScanningUseCase.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt @@ -1,12 +1,11 @@ -package com.tangem.feature.qrscanning.usecase +package com.tangem.domain.qrscanning.usecases import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.feature.qrscanning.SourceType -import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository import kotlinx.coroutines.flow.Flow -import java.lang.Exception class ListenToQrScanningUseCase( val repository: QrScanningEventsRepository, diff --git a/features/qr-scanning/impl/build.gradle.kts b/features/qr-scanning/impl/build.gradle.kts index e1d37506b4..5ad7d8346d 100644 --- a/features/qr-scanning/impl/build.gradle.kts +++ b/features/qr-scanning/impl/build.gradle.kts @@ -10,7 +10,6 @@ android { namespace = "com.tangem.feature.qrscanning.impl" } - dependencies { /** Core */ @@ -40,8 +39,13 @@ dependencies { implementation(deps.hilt.android) kapt(deps.hilt.kapt) + /** Api */ implementation(projects.features.qrScanning.api) + /** Domain */ + implementation(projects.domain.qrScanning) + implementation(projects.domain.qrScanning.models) + /** Compose */ implementation(deps.compose.foundation) implementation(deps.compose.ui) diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningModule.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningModule.kt deleted file mode 100644 index ad088e8c28..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningModule.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.feature.qrscanning.di - -import com.tangem.feature.qrscanning.repo.DefaultQrScanningEventsRepository -import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository -import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object QrScanningModule { - - @Provides - @Singleton - fun provideQrScannedEventsRepository(): QrScanningEventsRepository { - return DefaultQrScanningEventsRepository() - } - - @Provides - @Singleton - fun provideEmitQrScannedEventUseCase(repository: QrScanningEventsRepository): EmitQrScannedEventUseCase { - return EmitQrScannedEventUseCase(repository) - } -} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt index c4df653e5a..2f38d3e545 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.feature.qrscanning.presentation.transformers import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.feature.qrscanning.SourceType +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.impl.R import com.tangem.feature.qrscanning.presentation.QrScanningState import com.tangem.feature.qrscanning.viewmodel.QrScanningClickIntents diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt index 3ae564a745..a9c37e465c 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt @@ -1,7 +1,7 @@ package com.tangem.feature.qrscanning.viewmodel import androidx.activity.result.ActivityResultLauncher -import com.tangem.feature.qrscanning.SourceType +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter import kotlinx.coroutines.CoroutineScope import kotlin.properties.Delegates diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt index 7ecbdae562..64d837b7f4 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt @@ -1,8 +1,8 @@ package com.tangem.feature.qrscanning.viewmodel +import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase import com.tangem.feature.qrscanning.presentation.QrScanningStateController import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomSheetTransformer -import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.launch diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt index 5402f469b0..231081714b 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.feature.qrscanning.QrScanningRouter.Companion.NETWORK_KEY import com.tangem.feature.qrscanning.QrScanningRouter.Companion.SOURCE_KEY -import com.tangem.feature.qrscanning.SourceType +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter import com.tangem.feature.qrscanning.presentation.QrScanningState import com.tangem.feature.qrscanning.presentation.QrScanningStateController diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 062932fd25..eb9e87bdf6 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -69,12 +69,13 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.qrScanning) + implementation(projects.domain.qrScanning.models) /** Feature modules */ implementation(projects.features.send.api) implementation(projects.features.tokendetails.api) implementation(projects.features.qrScanning.api) - implementation(projects.features.qrScanning.impl) /** DI */ implementation(deps.hilt.android) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index db243390ea..0629979fd1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -5,10 +5,10 @@ import androidx.fragment.app.Fragment import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.feature.qrscanning.SourceType import com.tangem.features.send.impl.presentation.SendFragment import com.tangem.features.tokendetails.navigation.TokenDetailsRouter diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 539fd6d734..0a3521b1eb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -13,8 +13,8 @@ import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder -import com.tangem.feature.qrscanning.SourceType -import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.state.StateRouter diff --git a/settings.gradle.kts b/settings.gradle.kts index a7380d73d8..8c94ba65a8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -139,6 +139,8 @@ include(":domain:analytics") include(":domain:visa") include(":domain:onboarding") include(":domain:feedback") +include(":domain:qr-scanning") +include(":domain:qr-scanning:models") // endregion Domain modules // region Data modules @@ -158,4 +160,5 @@ include(":data:visa") include(":data:promo") include(":data:onboarding") include(":data:feedback") +include(":data:qr-scanning") // endregion Data modules \ No newline at end of file From 0f600e9dcfb5f8f0105a46880eebbecf0c1119ca Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Mar 2024 23:32:11 +0500 Subject: [PATCH 023/109] Updated on 2026-08-14 --- .../tap/di/domain/QrScanningDomainModule.kt | 7 + .../tap/features/send/ui/SendFragment.kt | 32 ++- data/qr-scanning/build.gradle.kts | 12 + .../DefaultQrScanningEventsRepository.kt | 123 +++++++++ .../DefaultQrScanningEventsRepositoryTest.kt | 253 ++++++++++++++++++ .../domain/common/extensions/Blockchain.kt | 16 ++ domain/qr-scanning/build.gradle.kts | 8 +- .../domain/qrscanning/models/QrResult.kt | 9 + .../repository/QrScanningEventsRepository.kt | 4 + .../qrscanning/usecases/ParseQrCodeUseCase.kt | 20 ++ .../presentation/viewmodel/SendViewModel.kt | 14 +- gradle/dependencies.toml | 2 +- 12 files changed, 489 insertions(+), 11 deletions(-) create mode 100644 data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt create mode 100644 domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt create mode 100644 domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt index 43f66670c6..ec86de8c46 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -24,4 +25,10 @@ internal object QrScanningDomainModule { fun provideEmitQrScannedEventUseCase(repository: QrScanningEventsRepository): EmitQrScannedEventUseCase { return EmitQrScannedEventUseCase(repository) } + + @Provides + @Singleton + fun provideParseQrCodeUseCase(repository: QrScanningEventsRepository): ParseQrCodeUseCase { + return ParseQrCodeUseCase(repository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 84628d0ffa..0402c5e2de 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -25,10 +25,14 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.qrscanning.models.QrResult import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.qrscanning.QrScanningRouter +import com.tangem.features.send.api.navigation.SendRouter.Companion.CRYPTO_CURRENCY_KEY import com.tangem.sdk.extensions.hideSoftKeyboard import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.analytics.events.Token @@ -60,6 +64,7 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import java.text.DecimalFormatSymbols import javax.inject.Inject @@ -82,11 +87,17 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { private val sendSubscriber = SendStateSubscriber(this) private lateinit var keyboardObserver: KeyboardObserver + private val cryptoCurrency: CryptoCurrency? + get() = arguments?.getParcelable(CRYPTO_CURRENCY_KEY) + val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind) @Inject lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase + @Inject + lateinit var parseQrCodeUseCase: ParseQrCodeUseCase + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) @@ -177,13 +188,21 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { listenToQrScanningUseCase(SourceType.SEND) .getOrElse { emptyFlow() } .flowWithLifecycle(this@SendFragment.lifecycle, minActiveState = Lifecycle.State.CREATED) - .collect { + .collect { rawQr -> delay(200) // Delayed launch is needed in order for the UI to be drawn and to process the sent events. // If do not use the delay, then etAmount error field is not displayed when // inserting an incorrect amount by shareUri - onCodeScanned(it) + cryptoCurrency?.let { cryptoCurrency -> + parseQrCodeUseCase(rawQr, cryptoCurrency = cryptoCurrency).fold( + ifLeft = { + onCodeScanned(QrResult(address = rawQr)) + Timber.w(it) + }, + ifRight = { onCodeScanned(it) }, + ) + } ?: onCodeScanned(QrResult(address = rawQr)) } } } @@ -254,15 +273,18 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { .launchIn(mainScope) } - private fun onCodeScanned(scannedCode: String) { - if (scannedCode.isEmpty()) return + private fun onCodeScanned(parsedQr: QrResult) { + if (parsedQr.address.isEmpty()) return store.dispatch( PasteAddress( - data = scannedCode, + data = parsedQr.address, sourceType = Token.Send.AddressEntered.SourceType.QRCode, ), ) + parsedQr.amount?.let { amount -> + store.dispatchOnMain(AmountAction.SetAmount(amount, isUserInput = false)) + } store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused)) } diff --git a/data/qr-scanning/build.gradle.kts b/data/qr-scanning/build.gradle.kts index ec2f620228..1c801589b7 100644 --- a/data/qr-scanning/build.gradle.kts +++ b/data/qr-scanning/build.gradle.kts @@ -15,8 +15,20 @@ dependencies { /** Domain */ implementation(projects.domain.qrScanning) implementation(projects.domain.qrScanning.models) + implementation(projects.domain.tokens.models) + + implementation(projects.core.ui) + + /** SdK */ + implementation(deps.tangem.blockchain) /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + /** Tests */ + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt index 1732996a6b..87729e8bc4 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt @@ -1,10 +1,15 @@ package com.tangem.data.qrscanning.repository +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.qrscanning.models.QrResult import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository +import com.tangem.domain.tokens.model.CryptoCurrency import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map +import java.math.BigDecimal +import java.net.URLDecoder internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository { @@ -19,4 +24,122 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository { override fun subscribeToScanningResults(type: SourceType) = scannedEvents .filter { it.type == type } .map { it.qrCode } + + override fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult { + val withoutSchema = stripSchema(qrCode, cryptoCurrency) + + // A poor man's ERC-681 parser: we want to extract only the destination address, and we don't care + // about other parts of the ERC-681 payload string like `chain_id` and/or `function_name`. + // + // We're extracting the destination address by parsing the given string until we meet + // any of the possible string delimiters (@ ? /). + val address = withoutSchema.takeWhile { char -> + char != CHAIN_DELIMITER && char != FUNCTION_DELIMITER && char != PARAM_DELIMITER + } + + val result = QrResult(address = address) + + extractParameters(withoutSchema) + .forEach { + when (it.key) { + Parameter.Amount -> { + // According to BIP-0021, the value is specified in decimals. No conversion needed + result.amount = it.value.toBigDecimalOrNull() + } + Parameter.Message, + Parameter.Memo, + -> { + result.memo = URLDecoder.decode(it.value, "UTF-8") + } + Parameter.Address -> { + // Overrides destination address for token transfers (ERC-681) + if (cryptoCurrency is CryptoCurrency.Token) { + // `address` parameter is used only if the contract address, encoded in the QR, + // matches the contract address of the token. + // Otherwise, the scanned string is likely malformed, and we stop the entire parsing routine + if (cryptoCurrency.contractAddress.equals(address, ignoreCase = true)) { + result.address = it.value + } else { + return QrResult() + } + } + } + Parameter.Value, + Parameter.Uint256, + -> { + // Extra convert parses scientific notation to decimal + // This is necessary to be able comparing BigDecimal values + result.amount = it.value.toBigDecimalOrNull() + ?.toPlainString()?.toBigDecimalOrNull() + ?.divide(BigDecimal.TEN.pow(cryptoCurrency.decimals)) + } + } + } + + return result + } + + private fun stripSchema(raw: String, currency: CryptoCurrency): String { + val qrSchemas = Blockchain.fromId(currency.network.id.value).getShareScheme() + + // The most specific (i.e. the most lengthy) prefixes always come first + qrSchemas + .sortedByDescending { it.length } + .forEach { schema -> + val stripped = raw.split(schema) + + if (stripped.size > 1) return stripped.last() + } + + return raw + } + + private fun extractParameters(from: String): Map { + val parametersBlock = from.substringAfter(PARAM_DELIMITER) + if (parametersBlock.isBlank()) return emptyMap() + + val paramList = parametersBlock.split(PARAMS_DELIMITER) + .mapNotNull { param -> + val parameterWithValue = param.split(PARAM_VALUE_DELIMITER) + if (parameterWithValue.size == 2) { + val name = Parameter.getParam(parameterWithValue.first()) + val value = parameterWithValue.last() + + if (name != null) { + name to value + } else { + null + } + } else { + null + } + }.associate { it } + + return paramList + } + + private enum class Parameter { + Amount, + Message, + Memo, + Address, + Value, + Uint256, + ; + + companion object { + fun getParam(name: String): Parameter? { + return Parameter.entries.firstOrNull { it.name.equals(name, ignoreCase = true) } + } + } + } + + private companion object { + // See https://eips.ethereum.org/EIPS/eip-681 for details. + const val CHAIN_DELIMITER = '@' // ERC-681 [ "@" chain_id ] + const val FUNCTION_DELIMITER = '/' // ERC-681 [ "/" function_name ] + const val PARAM_DELIMITER = '?' // BIP-021, ERC-681 + const val PARAMS_DELIMITER = '&' + const val PARAM_VALUE_DELIMITER = '=' + } } \ No newline at end of file diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt new file mode 100644 index 0000000000..ad1f6244a7 --- /dev/null +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt @@ -0,0 +1,253 @@ +package com.tangem.data.qrscanning + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.data.qrscanning.repository.DefaultQrScanningEventsRepository +import com.tangem.domain.qrscanning.models.QrResult +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import java.math.BigDecimal + +internal class DefaultQrScanningEventsRepositoryTest { + + private val repository = DefaultQrScanningEventsRepository() + + private val cryptoCurrencyId = mockk() + private val network = mockk() + private val cryptoCurrency = CryptoCurrency.Coin( + id = cryptoCurrencyId, + network = network, + name = "blockchain", + symbol = "symbol", + decimals = 18, + iconUrl = null, + isCustom = false, + ) + private val tokenCryptoCurrency = CryptoCurrency.Token( + id = cryptoCurrencyId, + network = network, + name = "blockchain", + symbol = "symbol", + decimals = 7, + iconUrl = null, + isCustom = false, + contractAddress = "0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7", + ) + + private val garbage = "some_garbage=" + + private val schema1 = "bitcoin" + private val schema2 = "ethereum" + + private val address1 = "bc1pw83rs5s75na2g7ec8yqgekr3ae209ye7ck2ftakjnh8tv3xzw8ls6wgt62" + private val address2 = "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb" + private val address3 = "pay-0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb" + private val address4 = "0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7" + + private val function = "/transfer" + + private val someParam = "someParam" + private val someParamValue = "someParamValue" + + private val addressParam = "address" + private val addressParamValue = "0xc00f86ab93cd0bd3a60213583d0fe35aaa1ace23" + + private val amountParam = "amount" + private val someAmountParamValue = "amount" + private val amountParamValue = "123.123" + + private val valueParam = "value" + private val someValueParamValue = "amount" + private val valueParamValue2 = "1.88e10" + private val valueParamValue3 = "1.68E11" + private val valueParamValue4 = "23000000000" + + private val memoParam = "memo" + private val memoParamValue = "a%20random%20memo" + private val memoParamValueUtf8 = "a random memo" + + private val messageParam = "message" + private val messageParamValue = "some%20message" + private val messageParamValueUft8 = "some message" + private val messageParamValue2 = "message" + + @Test + fun testBip021() { + every { network.id } returns Network.ID(Blockchain.Bitcoin.id) + positiveCase( + "$schema1:$address1", + QrResult(address = address1), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema1:$address1", + QrResult(address = address1), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue", + QrResult(address = address1), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$amountParam=$someAmountParamValue", + QrResult(address = address1), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$amountParam=$amountParamValue", + QrResult(address = address1, amount = BigDecimal(amountParamValue)), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$amountParam=$amountParamValue&$memoParam=$memoParamValue", + QrResult(address = address1, amount = BigDecimal(amountParamValue), memo = memoParamValueUtf8), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$messageParam=$messageParamValue", + QrResult(address = address1, memo = messageParamValueUft8), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$messageParam=$messageParamValue2", + QrResult(address = address1, memo = messageParamValue2), + cryptoCurrency, + ) + negativeCase( + "$address1?$someParam=$someParamValue&$amountParam=$amountParamValue", + QrResult(address = address1, memo = messageParamValue2), + cryptoCurrency, + ) + } + + @Test + fun testErc681Coin() { + every { network.id } returns Network.ID(Blockchain.Ethereum.id) + positiveCase( + address2, + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$schema2:$address2", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$schema2:$address3", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2$function?$addressParam=$addressParamValue", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$someAmountParamValue", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$valueParamValue2", + QrResult(address = address2, amount = BigDecimal("0.0000000188")), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$valueParamValue3", + QrResult(address = address2, amount = BigDecimal("0.000000168")), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$valueParamValue4", + QrResult(address = address2, amount = BigDecimal("0.000000023")), + cryptoCurrency, + ) + } + + @Test + fun testErc681Token() { + every { network.id } returns Network.ID(Blockchain.Ethereum.id) + positiveCase( + address2, + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$schema2:$address2", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$schema2:$address3", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$schema2:$address4$function?$addressParam=$addressParamValue", + QrResult(address = addressParamValue), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue&$valueParam=$someValueParamValue", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue&$valueParam=$valueParamValue2", + QrResult(address = address2, amount = BigDecimal("1880")), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue&$valueParam=$valueParamValue3", + QrResult(address = address2, amount = BigDecimal("16800")), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue&$valueParam=$valueParamValue4", + QrResult(address = address2, amount = BigDecimal("2300")), + tokenCryptoCurrency, + ) + negativeCase( + "$address2?$someParam=$someParamValue&$amountParam=$amountParamValue", + QrResult(address = address2, amount = BigDecimal("123.123"), memo = memoParamValueUtf8), + tokenCryptoCurrency, + ) + } + + private fun positiveCase(input: String, expected: QrResult, cryptoCurrency: CryptoCurrency) { + val actual = repository.parseQrCode(input, cryptoCurrency) + Truth.assertThat(actual.address).isEqualTo(expected.address) + Truth.assertThat(actual.amount).isEqualTo(expected.amount) + Truth.assertThat(actual.memo).isEqualTo(expected.memo) + } + + private fun negativeCase(input: String, expected: QrResult, cryptoCurrency: CryptoCurrency) { + val actual = repository.parseQrCode(input, cryptoCurrency) + Truth.assertThat(actual).isNotEqualTo(expected) + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 03841cc507..44e60f9690 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -93,6 +93,10 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "areon-network/test" -> Blockchain.AreonTestnet "pulsechain" -> Blockchain.PulseChain "pulsechain/test" -> Blockchain.PulseChainTestnet + "nexa" -> Blockchain.Nexa // FIXME + "nexa/testnet" -> Blockchain.NexaTestnet // FIXME + "zksync" -> Blockchain.ZkSyncEra // FIXME + "zksync/testnet" -> Blockchain.ZkSyncEraTestnet // FIXME else -> null } } @@ -187,6 +191,10 @@ fun Blockchain.toNetworkId(): String { Blockchain.AreonTestnet -> "areon-network/test" Blockchain.PulseChain -> "pulsechain" Blockchain.PulseChainTestnet -> "pulsechain/test" + Blockchain.ZkSyncEra -> "zksync" // FIXME + Blockchain.ZkSyncEraTestnet -> "zksync/testnet" // FIXME + Blockchain.Nexa -> "nexa" // FIXME + Blockchain.NexaTestnet -> "nexa/testnet" // FIXME } } @@ -249,6 +257,10 @@ fun Blockchain.toCoinId(): String { Blockchain.Aurora, Blockchain.AuroraTestnet -> "aurora-near" Blockchain.Areon, Blockchain.AreonTestnet -> "areon-network" Blockchain.PulseChain, Blockchain.PulseChainTestnet -> "pulsechain" + Blockchain.ZkSyncEra -> "zksync" // FIXME + Blockchain.ZkSyncEraTestnet -> "zksync/testnet" // FIXME + Blockchain.Nexa -> "nexa" // FIXME + Blockchain.NexaTestnet -> "nexa/testnet" // FIXME } } @@ -276,4 +288,8 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Playa3ull, + Blockchain.ZkSyncEra, + Blockchain.ZkSyncEraTestnet, + Blockchain.Nexa, + Blockchain.NexaTestnet, ) \ No newline at end of file diff --git a/domain/qr-scanning/build.gradle.kts b/domain/qr-scanning/build.gradle.kts index fe5d126eb1..f1ea5e63ec 100644 --- a/domain/qr-scanning/build.gradle.kts +++ b/domain/qr-scanning/build.gradle.kts @@ -1,12 +1,18 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } +android { + namespace = "com.tangem.domain.qrscanning" +} + dependencies { /** Domain */ implementation(projects.domain.qrScanning.models) + implementation(projects.domain.tokens.models) implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) diff --git a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt new file mode 100644 index 0000000000..a97a4a7161 --- /dev/null +++ b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.qrscanning.models + +import java.math.BigDecimal + +data class QrResult( + var address: String = "", + var amount: BigDecimal? = null, + var memo: String? = null, +) \ No newline at end of file diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt index b834c4dbcd..15ca99b951 100644 --- a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt @@ -1,6 +1,8 @@ package com.tangem.domain.qrscanning.repository +import com.tangem.domain.qrscanning.models.QrResult import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.tokens.model.CryptoCurrency import kotlinx.coroutines.flow.Flow interface QrScanningEventsRepository { @@ -8,4 +10,6 @@ interface QrScanningEventsRepository { suspend fun emitResult(type: SourceType, qrCode: String) fun subscribeToScanningResults(type: SourceType): Flow + + fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult } \ No newline at end of file diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt new file mode 100644 index 0000000000..0849c8bf1c --- /dev/null +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.qrscanning.usecases + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.qrscanning.models.QrResult +import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository +import com.tangem.domain.tokens.model.CryptoCurrency + +class ParseQrCodeUseCase( + val repository: QrScanningEventsRepository, +) { + operator fun invoke(qrCode: String, cryptoCurrency: CryptoCurrency): Either { + return try { + repository.parseQrCode(qrCode, cryptoCurrency).right() + } catch (e: Exception) { + e.left() + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index cf90cca81f..0617f6e83b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -13,6 +13,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.* @@ -32,7 +33,10 @@ import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.* +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.domain.wallets.usecase.ValidateWalletAddressUseCase +import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource @@ -75,12 +79,12 @@ internal class SendViewModel @Inject constructor( private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, - private val parseSharedAddressUseCase: ParseSharedAddressUseCase, private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val parseQrCodeUseCase: ParseQrCodeUseCase, currencyChecksRepository: CurrencyChecksRepository, isFeeApproximateUseCase: IsFeeApproximateUseCase, getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, @@ -178,6 +182,7 @@ internal class SendViewModel @Inject constructor( private var recipientsJobHolder = JobHolder() private var feeJobHolder = JobHolder() private var addressValidationJobHolder = JobHolder() + private var memoValidationJobHolder = JobHolder() private var sendNotificationsJobHolder = JobHolder() private var feeNotificationsJobHolder = JobHolder() private var qrScannerJobHolder = JobHolder() @@ -509,13 +514,14 @@ internal class SendViewModel @Inject constructor( // region recipient state clicks fun onRecipientAddressScanned(address: String) { viewModelScope.launch(dispatchers.main) { - parseSharedAddressUseCase(address, cryptoCurrency.network).fold( + parseQrCodeUseCase(address, cryptoCurrency).fold( ifRight = { parsedCode -> onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode) parsedCode.amount?.let { onAmountValueChange(it.toPlainString()) } parsedCode.memo?.let { onRecipientMemoValueChange(it) } }, ifLeft = { + onRecipientAddressValueChange(address, EnterAddressSource.QRCode) Timber.w(it) }, ) @@ -543,7 +549,7 @@ internal class SendViewModel @Inject constructor( val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty()) uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress) } - }.saveIn(addressValidationJobHolder) + }.saveIn(memoValidationJobHolder) } private suspend fun validateAddress(value: String): Boolean { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 96db197034..b7e7c03a4e 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,7 +85,7 @@ leakcanary = "2.13" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-514" +tangemBlockchainSdk = "develop-528" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-332" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 96fa67d0b17399aae05de63e3ee69e1dc6f26689 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Mar 2024 21:17:31 +0500 Subject: [PATCH 024/109] Updated on 2026-08-14 --- .../ui/components/fields/AmountTextField.kt | 5 +-- .../ui/components/fields/SimpleTextField.kt | 4 +-- .../state/amount/SendAmountStateConverter.kt | 29 ++++++++------- .../state/fields/SendAmountFieldConverter.kt | 4 ++- .../state/fields/SendTextField.kt | 1 + .../presentation/ui/amount/AmountField.kt | 10 ++++-- .../ui/amount/SendAmountContent.kt | 35 +++++++++++++------ 7 files changed, 57 insertions(+), 31 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index 4bf1b72d84..fc0ee052f3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -74,7 +75,7 @@ fun AmountTextField( }, modifier = modifier .background(TangemTheme.colors.background.action), - textStyle = textStyle, + textStyle = textStyle.copy(textDirection = TextDirection.ContentOrLtr), color = color, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, @@ -90,7 +91,7 @@ fun AmountTextField( } Text( text = placeholder, - style = textStyle, + style = textStyle.copy(textDirection = TextDirection.ContentOrLtr), color = TangemTheme.colors.text.disabled, textAlign = placeholderTextAlign, modifier = Modifier diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt index 4a3fdbff20..6d31ebeaef 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -67,9 +67,7 @@ fun SimpleTextField( LaunchedEffect(key1 = isSelectionChanged) { if (isSelectionChanged) { - textFieldValueState = textFieldValue.copy( - selection = getValueRange(value), - ) + textFieldValueState = textFieldValue } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt index bbadf98185..e1c163e2d4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt @@ -14,6 +14,7 @@ import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.persistentListOf internal class SendAmountStateConverter( @@ -37,18 +38,22 @@ internal class SendAmountStateConverter( tokenIconState = iconStateConverter.convert(status), amountTextField = sendAmountFieldConverter.convert(value), isPrimaryButtonEnabled = false, - segmentedButtonConfig = persistentListOf( - SendAmountSegmentedButtonsConfig( - title = stringReference(status.currency.symbol), - iconState = iconStateConverter.convert(status), - isFiat = false, - ), - SendAmountSegmentedButtonsConfig( - title = stringReference(appCurrency.code), - iconUrl = appCurrency.iconSmallUrl, - isFiat = true, - ), - ), + segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) { + persistentListOf() + } else { + persistentListOf( + SendAmountSegmentedButtonsConfig( + title = stringReference(status.currency.symbol), + iconState = iconStateConverter.convert(status), + isFiat = false, + ), + SendAmountSegmentedButtonsConfig( + title = stringReference(appCurrency.code), + iconUrl = appCurrency.iconSmallUrl, + isFiat = true, + ), + ) + }, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt index ff622738f7..37bdcf2eae 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt @@ -31,10 +31,11 @@ internal class SendAmountFieldConverter( val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val cryptoDecimal = value.toBigDecimalOrDefault() val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency) + val fiatRate = cryptoCurrencyStatus.value.fiatRate val fiatValue = if (value.isEmpty()) { "" } else { - val fiatDecimal = cryptoCurrencyStatus.value.fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO + val fiatDecimal = fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO fiatDecimal.parseBigDecimal(FIAT_DECIMALS) } val isDoneActionEnabled = !cryptoDecimal.isZero() @@ -52,6 +53,7 @@ internal class SendAmountFieldConverter( fiatAmount = getAppCurrencyAmount(appCurrencyProvider()), isError = false, error = TextReference.Res(R.string.swapping_insufficient_funds), + isFiatUnavailable = fiatRate == null, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt index 97682a9e21..8db8a2fc02 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt @@ -27,6 +27,7 @@ internal sealed class SendTextField { val fiatAmount: Amount, val isFiatValue: Boolean, val fiatValue: String, + val isFiatUnavailable: Boolean, val isError: Boolean, val error: TextReference, ) : SendTextField() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt index bd6b8b54e1..d9ef26bd3c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -11,10 +11,12 @@ import androidx.compose.ui.Alignment.Companion.BottomCenter import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDirection import com.tangem.core.ui.components.fields.AmountTextField import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.defaultFormat import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.send.impl.presentation.state.fields.SendTextField @@ -62,10 +64,14 @@ internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean) end = TangemTheme.dimens.spacing12, ), ) { - val text = "${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}" + val text = if (sendField.isFiatUnavailable) { + BigDecimalFormatter.EMPTY_BALANCE_SIGN + } else { + "${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}" + } Text( text = text, - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr), color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, modifier = Modifier diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt index fa0a7445eb..143b2e1e16 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt @@ -7,15 +7,17 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.currency.fiaticon.FiatIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme - import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig @@ -28,6 +30,7 @@ internal fun SendAmountContent( clickIntents: SendClickIntents, ) { if (amountState == null) return + val hapticFeedback = LocalHapticFeedback.current Column( modifier = Modifier .background(TangemTheme.colors.background.tertiary), @@ -41,19 +44,29 @@ internal fun SendAmountContent( end = TangemTheme.dimens.spacing16, ), ) { - SegmentedButtons( - modifier = Modifier - .weight(1f) - .height(TangemTheme.dimens.size40), - config = amountState.segmentedButtonConfig, - showIndication = false, - onClick = { clickIntents.onCurrencyChangeClick(it.isFiat) }, - ) { - SendAmountCurrencyButton(it) + if (amountState.segmentedButtonConfig.isNotEmpty()) { + SegmentedButtons( + modifier = Modifier + .weight(1f) + .height(TangemTheme.dimens.size40), + config = amountState.segmentedButtonConfig, + showIndication = false, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + clickIntents.onCurrencyChangeClick(it.isFiat) + }, + ) { + SendAmountCurrencyButton(it) + } + } else { + SpacerWMax() } SecondaryButton( text = stringResource(R.string.send_max_amount), - onClick = clickIntents::onMaxValueClick, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + clickIntents.onMaxValueClick() + }, size = TangemButtonSize.Text, shape = RoundedCornerShape(TangemTheme.dimens.radius26), modifier = Modifier From 0ed70b73330cc1fe62ad0c5641022fea0d2a92d0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Mar 2024 12:33:05 +0100 Subject: [PATCH 025/109] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 16 +- .../src/main/res/values-zh-rTW/strings.xml | 4 - core/res/src/main/res/values/strings.xml | 22 +-- .../wallet2/ui/YourSeedPhraseScreen.kt | 25 +++ .../referral/ui/ParticipateBottomBlock.kt | 174 ++++++++++-------- 5 files changed, 140 insertions(+), 101 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 562af4dcb0..79539f13a8 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -3,8 +3,6 @@ Добавить токен Валюты Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. - Спасибо за ваш отзыв - Отправлено успешно Обратиться в поддержку Эта функция недоступна в демонстрационном режиме Причина: %s @@ -53,7 +51,6 @@ Заводские настройки Тип безопасности Настройки карты - Чат Принять Доступ запрещен Применить @@ -79,7 +76,6 @@ Включено Ошибка Обозреватель - Посмотреть историю Посмотреть историю транзакций Обозреватель Комиссия @@ -100,6 +96,7 @@ Нет адреса OK Основная карта + Кодовая фраза Подробнее Получить Отклонить @@ -134,7 +131,6 @@ Добавить токен Адрес контракта Адрес контракта некорректен - Путь деривации некорректен Пожалуйста, выберите сеть Десятичное число должно быть действительным целым числом, до %li Своя деривация @@ -288,6 +284,7 @@ Ошибка активации Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить? Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас. + Кодовая фраза — это расширенная функция безопасности, которую используют криптокошельки. Она добавляет дополнительное слово или фразу по вашему выбору к уже существующей seed - фразе, чтобы разблокировать совершенно новый набор адресов. Добавить резервную карту Сканировать карту #%d Создать резервную копию @@ -368,11 +365,7 @@ По балансу Сортировка токенов Список - Отправить отзыв - Оцените приложение 🙌 - Поделитесь опытом использования приложения - нам важна любая деталь. Нравится наше приложение? - %1$s %2$s адрес в сети %3$s %1$s (%2$s) в сети %3$s Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств. Участвовать @@ -435,7 +428,7 @@ Транзакция не выполнена Сумма Подтвердить - %1$s в %2$s + %1$s, %2$s Адрес Код назначения Введите адрес @@ -525,6 +518,7 @@ Балансы скрыты Балансы показаны Отменить + Сгенерировать XPUB Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. Скрыть %s @@ -656,6 +650,8 @@ Отправка средств станет доступной после завершения транзакции(-ий) в сети %s Отправка средств станет доступной после завершения транзакции %s Транзакция в обработке + Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку. + Оповещение сети Солана Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже. Некоторые сети недоступны diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index da5557d604..a8a013bebc 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -3,8 +3,6 @@ 添加自定義代幣 管理代幣 僅將 %1$s (%2$s) 從 %3$s 網絡發送到此地址。使用其他代幣和網絡可能會導致資金損失 - 謝謝您的反饋 - 成功送出 請求支持 此功能不在展示模式中提供 原因:%s @@ -37,7 +35,6 @@ 回復至原廠設置 安全模式 卡片設置 - 支援 接受 注意 餘額: %s @@ -90,7 +87,6 @@ 添加代幣 代幣地址 無效地址 - 衍生路徑錯誤 請選擇網路 小數點 默認 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 4f8f87c686..d91eeb01d1 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -3,8 +3,6 @@ Add custom token Manage tokens Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. - Thank you for your feedback - Sent successfully Request support This feature is disabled in Demo mode Reason: %s @@ -52,7 +50,6 @@ Reset to Factory Settings Security Mode Card Settings - Support Accept Access denied Apply @@ -78,7 +75,6 @@ Enabled Error Explore - Explore history Explore transaction history Explorer Fee @@ -99,6 +95,7 @@ No address OK Primary Card + Passphrase Read more Receive Reject @@ -133,7 +130,6 @@ Add token Contract address Contract address is invalid - Derivation path is invalid Please select the network Decimal must be a valid integer, up to %li Custom derivation @@ -288,6 +284,7 @@ Activation error You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process? The backup process is partly complete. You can\'t exit it now. + The passphrase is an advanced security feature that crypto wallets use. It adds an extra word or phrase of your own choosing to your already existing recovery phrase to unlock a brand-new set of addresses. Add a backup card Scan the card #%d Backup now @@ -368,11 +365,7 @@ Settings You have not given access to your camera Camera access denied - Share feedback - Rate the app 🙌 - Share your app experience – every detail counts. Enjoying our app? - %1$s %2$s address on %3$s network %1$s (%2$s) on %3$s network Send only %s to this address. Sending any other currency will result in its irreversible loss. Participate @@ -431,7 +424,7 @@ The transaction is not completed Amount Confirm - %1$s at %2$s + %1$s, %2$s Address Destination Tag Enter address @@ -486,7 +479,9 @@ Send to A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds My wallets - Sending... + Tap any field to change it + Send %s + You are sending %1$s including a network fee of %2$s Sending %s Total %1$s and %2$s will be sent @@ -533,6 +528,7 @@ Balances hidden Balances shown Undo + Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. Hide %s @@ -612,7 +608,7 @@ Tangem Use %s or scan a card to unlock access to your wallet Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost. - Backup error + Activation error Could be better Like it Ok, Got it! @@ -662,6 +658,8 @@ Sending funds will be available once the pending transaction(s) in network %s is complete Sending funds will be available once the %s transaction is complete Transaction pending + The Solana network is congested. If your transaction is not processed within 2 minutes, please repeat the transaction. + Solana Network Alert Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free. Some networks currently are unreachable. Please try again later. Some networks are unreachable diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt index 76a7e48923..23e21b9c92 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid @@ -8,16 +9,19 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SpacerW32 import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.R +import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState import com.tangem.feature.onboarding.presentation.wallet2.model.MnemonicGridItem import com.tangem.feature.onboarding.presentation.wallet2.model.YourSeedPhraseState import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList /** [REDACTED_AUTHOR] @@ -88,4 +92,25 @@ private fun PhraseGreedBlock(mnemonicGridItems: ImmutableList) } } } +} + +@Preview +@Composable +private fun YourSeedPhraseScreenPreview_Light() { + TangemTheme(isDark = false) { + YourSeedPhraseScreen( + state = YourSeedPhraseState( + mnemonicGridItems = (1..12).map { MnemonicGridItem(it, it.toString()) }.toImmutableList(), + buttonContinue = ButtonState( + enabled = true, + isClickable = true, + showProgress = false, + onClick = {}, + ), + ), + modifier = Modifier + .fillMaxSize() + .background(color = TangemTheme.colors.background.primary), + ) + } } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt index 57337c925a..3417b6931f 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -27,6 +27,8 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.core.content.ContextCompat.startActivity import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme @@ -113,14 +115,22 @@ private fun Awards(expectedAwards: ExpectedAwards) { thickness = TangemTheme.dimens.size0_5, ) AwardText( - startText = stringResource(id = R.string.referral_expected_awards), + startText = if (expectedAwards.expectedAwards.isNotEmpty()) { + stringResource(id = R.string.referral_expected_awards) + } else { + stringResource(id = R.string.referral_no_expected_awards) + }, startTextColor = TangemTheme.colors.text.tertiary, startTextStyle = TangemTheme.typography.subtitle2, - endText = pluralStringResource( - id = R.plurals.referral_number_of_wallets, - count = expectedAwards.numberOfWallets, - expectedAwards.numberOfWallets, - ), + endText = if (expectedAwards.expectedAwards.isNotEmpty()) { + pluralStringResource( + id = R.plurals.referral_number_of_wallets, + count = expectedAwards.numberOfWallets, + expectedAwards.numberOfWallets, + ) + } else { + "" + }, endTextColor = TangemTheme.colors.text.tertiary, endTextStyle = TangemTheme.typography.body2, cornersToRound = CornersToRound.ZERO, @@ -309,34 +319,20 @@ private fun Context.shareText(text: String) { @Preview(widthDp = 360, showBackground = true) @Composable -private fun Preview_ParticipateBottomBlock_InLightTheme() { +private fun ParticipateBottomBlockPreview_Light( + @PreviewParameter(ParticipateBottomBlockDataProvider::class) data: ParticipateBottomBlockData, +) { TangemTheme(isDark = false) { Column(Modifier.background(TangemTheme.colors.background.secondary)) { ParticipateBottomBlock( - purchasedWalletCount = 3, - code = "x4JdK", - shareLink = "", - expectedAwards = ExpectedAwards( - numberOfWallets = 3, - expectedAwards = listOf( - ExpectedAward( - amount = "10 USDT", - paymentDate = "Today", - ), - ExpectedAward( - amount = "20 USDT", - paymentDate = "6 Aug 2023", - ), - ExpectedAward( - amount = "30 USDT", - paymentDate = "10 Aug 2023", - ), - ), - ), - onAgreementClick = {}, - onShowCopySnackbar = {}, - onCopyClick = {}, - onShareClick = {}, + purchasedWalletCount = data.purchasedWalletCount, + code = data.code, + shareLink = data.shareLink, + expectedAwards = data.expectedAwards, + onAgreementClick = data.onAgreementClick, + onShowCopySnackbar = data.onShowCopySnackbar, + onCopyClick = data.onCopyClick, + onShareClick = data.onShareClick, ) } } @@ -344,18 +340,20 @@ private fun Preview_ParticipateBottomBlock_InLightTheme() { @Preview(widthDp = 360, showBackground = true) @Composable -private fun Preview_ParticipateBottomBlock_Without_Awards_InLightTheme() { - TangemTheme(isDark = false) { +private fun ParticipateBottomBlockPreview_Dark( + @PreviewParameter(ParticipateBottomBlockDataProvider::class) state: ParticipateBottomBlockData, +) { + TangemTheme(isDark = true) { Column(Modifier.background(TangemTheme.colors.background.secondary)) { ParticipateBottomBlock( - purchasedWalletCount = 3, - code = "x4JdK", - shareLink = "", - expectedAwards = null, - onAgreementClick = {}, - onShowCopySnackbar = {}, - onCopyClick = {}, - onShareClick = {}, + purchasedWalletCount = state.purchasedWalletCount, + code = state.code, + shareLink = state.shareLink, + expectedAwards = state.expectedAwards, + onAgreementClick = state.onAgreementClick, + onShowCopySnackbar = state.onShowCopySnackbar, + onCopyClick = state.onCopyClick, + onShareClick = state.onShareClick, ) } } @@ -363,26 +361,7 @@ private fun Preview_ParticipateBottomBlock_Without_Awards_InLightTheme() { @Preview(widthDp = 360, showBackground = true) @Composable -private fun Preview_ParticipateBottomBlock_Without_Awards_And_Purchased_Wallets_InLightTheme() { - TangemTheme(isDark = false) { - Column(Modifier.background(TangemTheme.colors.background.secondary)) { - ParticipateBottomBlock( - purchasedWalletCount = 0, - code = "x4JdK", - shareLink = "", - expectedAwards = null, - onAgreementClick = {}, - onShowCopySnackbar = {}, - onCopyClick = {}, - onShareClick = {}, - ) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun LessMoreButton_White() { +private fun LessMoreButton_Light() { TangemTheme(isDark = false) { LessMoreButton( isExpanded = remember { @@ -394,19 +373,64 @@ private fun LessMoreButton_White() { @Preview(widthDp = 360, showBackground = true) @Composable -private fun Preview_ParticipateBottomBlock_Without_Awards_InDarkTheme() { +private fun LessMoreButton_Dark() { TangemTheme(isDark = true) { - Column(Modifier.background(TangemTheme.colors.background.secondary)) { - ParticipateBottomBlock( - purchasedWalletCount = 3, - code = "x4JdK", - shareLink = "", - expectedAwards = null, - onAgreementClick = {}, - onShowCopySnackbar = {}, - onCopyClick = {}, - onShareClick = {}, - ) - } + LessMoreButton( + isExpanded = remember { + mutableStateOf(false) + }, + ) } -} \ No newline at end of file +} + +private class ParticipateBottomBlockDataProvider : CollectionPreviewParameterProvider( + collection = listOf( + ParticipateBottomBlockData( + purchasedWalletCount = 3, + expectedAwards = ExpectedAwards( + numberOfWallets = 3, + expectedAwards = listOf( + ExpectedAward( + amount = "10 USDT", + paymentDate = "Today", + ), + ExpectedAward( + amount = "20 USDT", + paymentDate = "6 Aug 2023", + ), + ExpectedAward( + amount = "30 USDT", + paymentDate = "10 Aug 2023", + ), + ), + ), + ), + ParticipateBottomBlockData( + purchasedWalletCount = 3, + expectedAwards = ExpectedAwards( + numberOfWallets = 3, + expectedAwards = emptyList(), + ), + ), + ParticipateBottomBlockData( + purchasedWalletCount = 3, + expectedAwards = null, + ), + ParticipateBottomBlockData( + purchasedWalletCount = 0, + expectedAwards = null, + ), + + ), +) + +private data class ParticipateBottomBlockData( + val purchasedWalletCount: Int, + val expectedAwards: ExpectedAwards?, + val code: String = "x4JDK", + val shareLink: String = "", + val onAgreementClick: () -> Unit = {}, + val onShowCopySnackbar: () -> Unit = {}, + val onCopyClick: () -> Unit = {}, + val onShareClick: () -> Unit = {}, +) \ No newline at end of file From 8d16f06c0890210e60bdb2e778cc8e871d5c765f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Mar 2024 18:43:13 +0000 Subject: [PATCH 026/109] Updated on 2026-08-14 --- .../model/UserWalletInformation.kt | 1 + .../tap/domain/userWalletList/utils/Mapper.kt | 2 + .../features/onboarding/OnboardingHelper.kt | 16 ++++-- .../wallet/redux/OnboardingWalletAction.kt | 49 ++++++++++--------- .../redux/OnboardingWalletMiddleware.kt | 6 +++ .../wallet/redux/OnboardingWalletReducer.kt | 1 + .../wallet/redux/OnboardingWalletState.kt | 1 + .../card/DefaultDerivationsRepositoryTest.kt | 1 + .../domain/userwallets/UserWalletBuilder.kt | 9 ++++ .../com/tangem/domain/models/scan/CardDTO.kt | 2 +- .../domain/wallets/models/UserWallet.kt | 1 + .../analytics/WalletScreenAnalyticsEvent.kt | 44 +++++++++-------- .../utils/WalletWarningsAnalyticsSender.kt | 1 + .../wallet/domain/BackupValidator.kt | 42 ++++++++++++++++ .../domain/GetMultiWalletWarningsFactory.kt | 12 ++++- .../wallet/state/model/WalletNotification.kt | 9 +++- 16 files changed, 142 insertions(+), 55 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt index fe064832d6..8a3f2fe559 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt @@ -18,4 +18,5 @@ internal data class UserWalletPublicInformation( val cardsInWallet: Set, val scanResponse: ScanResponse, val isMultiCurrency: Boolean, + val hasBackupError: Boolean = false, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt index 4116cde096..451e717e9e 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt @@ -20,6 +20,7 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation wallets = emptyList(), ), ), + hasBackupError = hasBackupError, ) internal fun UserWalletPublicInformation.toUserWallet(): UserWallet { @@ -30,6 +31,7 @@ internal fun UserWalletPublicInformation.toUserWallet(): UserWallet { cardsInWallet = cardsInWallet, scanResponse = scanResponse, isMultiCurrency = isMultiCurrency, + hasBackupError = hasBackupError, ) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index fd59efff07..dee9d0f733 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -72,13 +72,14 @@ object OnboardingHelper { scanResponse: ScanResponse, accessCode: String? = null, backupCardsIds: List? = null, + hasBackupError: Boolean = false, ) { Analytics.setContext(scanResponse) scope.launch { when { // When should save user wallets, then save card without navigate to save wallet screen store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> { - proceedWithScanResponse(scanResponse, backupCardsIds) + proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) store.dispatchOnMain( SaveWalletAction.ProvideBackupInfo( @@ -92,7 +93,7 @@ object OnboardingHelper { // When should not save user wallets but device has biometry and save wallet screen has not been shown, // then open save wallet screen tangemSdkManager.canUseBiometry && preferencesStorage.shouldShowSaveUserWalletScreen -> { - proceedWithScanResponse(scanResponse, backupCardsIds) + proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) delay(timeMillis = 1_200) @@ -109,7 +110,7 @@ object OnboardingHelper { } // If device has no biometry and save wallet screen has been shown, then go through old scenario else -> { - proceedWithScanResponse(scanResponse, backupCardsIds) + proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) } } @@ -129,8 +130,13 @@ object OnboardingHelper { } } - private suspend fun proceedWithScanResponse(scanResponse: ScanResponse, backupCardsIds: List?) { - val userWallet = UserWalletBuilder(scanResponse) + private suspend fun proceedWithScanResponse( + scanResponse: ScanResponse, + backupCardsIds: List?, + hasBackupError: Boolean, + ) { + val userWallet = UserWalletBuilder(scanResponse = scanResponse) + .hasBackupError(hasBackupError) .backupCardsIds(backupCardsIds?.toSet()) .build() .guard { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt index 6b536fc754..5bd2df0285 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt @@ -9,23 +9,23 @@ import kotlinx.coroutines.CoroutineScope import org.rekotlin.Action sealed class OnboardingWalletAction : Action { - object Init : OnboardingWalletAction() - object GetToCreateWalletStep : OnboardingWalletAction() - object CreateWallet : OnboardingWalletAction() + data object Init : OnboardingWalletAction() + data object GetToCreateWalletStep : OnboardingWalletAction() + data object CreateWallet : OnboardingWalletAction() data class WalletWasCreated( val shouldSendAnalyticsEvent: Boolean, val result: CompletionResult, ) : OnboardingWalletAction() - object Done : OnboardingWalletAction() + data object Done : OnboardingWalletAction() data class FinishOnboarding(val scope: CoroutineScope) : OnboardingWalletAction() - object ResumeBackup : OnboardingWalletAction() + data object ResumeBackup : OnboardingWalletAction() data class LoadArtwork(val cardArtworkUriForUnfinishedBackup: Uri? = null) : OnboardingWalletAction() class SetArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction() - object OnBackPressed : OnboardingWalletAction() + data object OnBackPressed : OnboardingWalletAction() } sealed class OnboardingWallet2Action : OnboardingWalletAction() { @@ -49,45 +49,46 @@ sealed class OnboardingWallet2Action : OnboardingWalletAction() { sealed class BackupAction : Action { - object IntroduceBackup : BackupAction() - object StartBackup : BackupAction() - object SkipBackup : BackupAction() + data object IntroduceBackup : BackupAction() + data object StartBackup : BackupAction() + data object SkipBackup : BackupAction() - object StartAddingPrimaryCard : BackupAction() - object ScanPrimaryCard : BackupAction() + data object ErrorInBackupCard : BackupAction() + data object StartAddingPrimaryCard : BackupAction() + data object ScanPrimaryCard : BackupAction() /** * Check for unfinished backup of standard Wallets * See more GlobalAction.Onboarding.StartForUnfinishedBackup */ - object CheckForUnfinishedBackup : BackupAction() + data object CheckForUnfinishedBackup : BackupAction() - object StartAddingBackupCards : BackupAction() - object AddBackupCard : BackupAction() { - object Success : BackupAction() + data object StartAddingBackupCards : BackupAction() + data object AddBackupCard : BackupAction() { + data object Success : BackupAction() data class ChangeButtonLoading(val isLoading: Boolean) : BackupAction() } - object FinishAddingBackupCards : BackupAction() + data object FinishAddingBackupCards : BackupAction() - object ShowAccessCodeInfoScreen : BackupAction() - object ShowEnterAccessCodeScreen : BackupAction() + data object ShowAccessCodeInfoScreen : BackupAction() + data object ShowEnterAccessCodeScreen : BackupAction() data class CheckAccessCode(val accessCode: String) : BackupAction() data class SetAccessCodeError(val error: AccessCodeError?) : BackupAction() data class SaveFirstAccessCode(val accessCode: String) : BackupAction() data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction() - object OnAccessCodeDialogClosed : BackupAction() + data object OnAccessCodeDialogClosed : BackupAction() - object PrepareToWritePrimaryCard : BackupAction() - object WritePrimaryCard : BackupAction() + data object PrepareToWritePrimaryCard : BackupAction() + data object WritePrimaryCard : BackupAction() data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction() data class WriteBackupCard(val cardNumber: Int) : BackupAction() data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction() - object DiscardBackup : BackupAction() - object DiscardSavedBackup : BackupAction() - object ResumeFoundUnfinishedBackup : BackupAction() + data object DiscardBackup : BackupAction() + data object DiscardSavedBackup : BackupAction() + data object ResumeFoundUnfinishedBackup : BackupAction() data class ResetBackupCard(val cardId: String) : BackupAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 460734903f..16045b38c8 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -20,6 +20,7 @@ import com.tangem.domain.userwallets.Artwork import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource +import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.operations.backup.BackupService import com.tangem.tap.* @@ -173,6 +174,7 @@ private fun handleWalletAction(action: Action) { scanResponse = updatedScanResponse, accessCode = backupState.accessCode, backupCardsIds = backupState.backupCardIds, + hasBackupError = backupState.hasBackupError, ) } } @@ -484,6 +486,10 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) backupService.proceedBackup { result -> when (result) { is CompletionResult.Success -> { + val backupValidator = BackupValidator() + if (!backupValidator.isValid(CardDTO(result.data))) { + store.dispatchOnMain(BackupAction.ErrorInBackupCard) + } if (backupService.currentState == BackupService.State.Finished) { store.dispatchOnMain(BackupAction.FinishBackup()) } else { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt index 61bee3be0f..1a400ea778 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt @@ -112,6 +112,7 @@ private object BackupReducer { } else { state.copy(backupStep = BackupStep.WriteBackupCard(action.cardNumber)) } + is BackupAction.ErrorInBackupCard -> state.copy(hasBackupError = true) is BackupAction.SkipBackup -> state.copy(backupStep = BackupStep.Finished) is BackupAction.FinishBackup -> state.copy(backupStep = BackupStep.Finished) BackupAction.OnAccessCodeDialogClosed -> state.copy(backupStep = BackupStep.AddBackupCards) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt index 0920fb4a9b..64b81fc771 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt @@ -70,6 +70,7 @@ data class BackupState( val canSkipBackup: Boolean = true, val isInterruptedBackup: Boolean = false, val showBtnLoading: Boolean = false, + val hasBackupError: Boolean = false, ) enum class AccessCodeError { diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt index 730ddb8d96..b1e1189c8e 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt @@ -37,6 +37,7 @@ internal class DefaultDerivationsRepositoryTest { cardsInWallet = setOf(), isMultiCurrency = false, scanResponse = ScanResponseMockFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()), + hasBackupError = false, ) @Test diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt index 182653ef00..325fb2efa7 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt @@ -11,6 +11,7 @@ class UserWalletBuilder( private val getCardImageUseCase: GetCardImageUseCase = GetCardImageUseCase(), ) { private var backupCardsIds: Set = emptySet() + private var hasBackupError: Boolean = false private val CardDTO.isBackupNotAllowed: Boolean get() = !this.settings.isBackupAllowed @@ -41,6 +42,13 @@ class UserWalletBuilder( } } + /** + * Sets if UserWallet has any backup errors (wrong curves etc). Use in onboarding + */ + fun hasBackupError(hasBackupError: Boolean) = this.apply { + this.hasBackupError = hasBackupError + } + suspend fun build(): UserWallet? { return with(scanResponse) { UserWalletIdBuilder.scanResponse(scanResponse) @@ -53,6 +61,7 @@ class UserWalletBuilder( cardsInWallet = backupCardsIds.plus(card.cardId), scanResponse = this, isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(), + hasBackupError = hasBackupError, ) } } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt index 8863fe8a03..ff2b5be1d0 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt @@ -303,7 +303,7 @@ data class CardDTO( data class Active(val cardCount: Int) : BackupStatus() - object NoBackup : BackupStatus() + data object NoBackup : BackupStatus() val isActive: Boolean get() = this is Active diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt index 710566e799..4904076698 100644 --- a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt @@ -20,6 +20,7 @@ data class UserWallet( val artworkUrl: String, val cardsInWallet: Set, val isMultiCurrency: Boolean, + val hasBackupError: Boolean, val scanResponse: ScanResponse, // TODO: Replace with [com.tangem.domain.models.scan.CardDTO] ) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index 2717ca2dfa..0b2b604294 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -23,7 +23,7 @@ sealed class WalletScreenAnalyticsEvent { override val oneTimeEventId: String = id + userWalletId.stringValue } - object WalletOpened : Basic(event = "Wallet Opened") + data object WalletOpened : Basic(event = "Wallet Opened") class CardWasScanned(source: AnalyticsParam.ScannedFrom) : Basic( event = "Card Was Scanned", @@ -45,8 +45,8 @@ sealed class WalletScreenAnalyticsEvent { params: Map = mapOf(), ) : AnalyticsEvent(category = "Main Screen", event = event, params = params) { - object ScreenOpened : MainScreen(event = "Screen opened") - object WalletSwipe : MainScreen(event = "Wallet Swipe") + data object ScreenOpened : MainScreen(event = "Screen opened") + data object WalletSwipe : MainScreen(event = "Wallet Swipe") class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen( event = "Enable Biometric", @@ -58,37 +58,39 @@ sealed class WalletScreenAnalyticsEvent { params = mapOf("Result" to result.value), ) - object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped") - object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped") - object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked") - object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped") + data object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped") + data object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped") + data object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked") + data object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped") - object NetworksUnreachable : MainScreen(event = "Notice - Networks Unreachable") + data object NetworksUnreachable : MainScreen(event = "Notice - Networks Unreachable") - object MissingAddresses : MainScreen(event = "Notice - Missing Addresses") + data object MissingAddresses : MainScreen(event = "Notice - Missing Addresses") - object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions") + data object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions") - object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem") + data object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem") - object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card") + data object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card") - object TestnetCard : MainScreen(event = "Notice - Testnet Card") + data object TestnetCard : MainScreen(event = "Notice - Testnet Card") - object DemoCard : MainScreen(event = "Notice - Demo Card") + data object DemoCard : MainScreen(event = "Notice - Demo Card") - object DevelopmentCard : MainScreen(event = "Notice - Development Card") + data object DevelopmentCard : MainScreen(event = "Notice - Development Card") - object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock") + data object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock") - object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet") + data object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet") - object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics") + data object BackupError : MainScreen(event = "Notice - Backup Error") - object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan") + data object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics") - object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped") + data object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan") - object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped") + data object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped") + + data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped") } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 899dd5a09a..e80fc87a24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -43,6 +43,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem + is WalletNotification.Critical.BackupError -> MainScreen.BackupError is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender] is WalletNotification.Informational.NoAccount, is WalletNotification.Warning.LowSignatures, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt new file mode 100644 index 0000000000..caa4c9fd87 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt @@ -0,0 +1,42 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.common.configs.CardConfig +import com.tangem.domain.models.scan.CardDTO +import javax.inject.Inject + +class BackupValidator @Inject constructor() { + + fun isValid(cardDTO: CardDTO): Boolean { + return validateBackupStatus(cardDTO) && validateCurves(cardDTO) + } + + private fun validateCurves(cardDTO: CardDTO): Boolean { + val config = CardConfig.createConfig(cardDTO) + // / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets, + // we cannot determine whether this curve is missing due to an error or because the user + // did not want to recreate the wallet. + val expectedCurves = config.mandatoryCurves + .filterNot { it == EllipticCurve.Bls12381G2Aug } + val curves = cardDTO.wallets.map { it.curve } + for (expectedCurve in expectedCurves) { + val cardCurvesCount = curves.count { it == expectedCurve } + + // missing curve + if (cardCurvesCount == 0) { + return false + } + + // duplicated curve + if (cardCurvesCount > 1) { + return false + } + } + return true + } + + private fun validateBackupStatus(cardDTO: CardDTO): Boolean { + val backupStatus = cardDTO.backupStatus + return backupStatus != null && backupStatus !is CardDTO.BackupStatus.CardLinked + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 7de96e0c75..9fceb87f55 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -14,6 +14,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.repository.PromoRepository +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification @@ -36,6 +37,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, private val promoRepository: PromoRepository, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, + private val backupValidator: BackupValidator, ) { private var readyForRateAppNotification = false @@ -64,7 +66,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( buildList { addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents) - addCriticalNotifications(cardTypesResolver) + addCriticalNotifications(userWallet) addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents) @@ -92,7 +94,13 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun MutableList.addCriticalNotifications(cardTypesResolver: CardTypesResolver) { + private fun MutableList.addCriticalNotifications(userWallet: UserWallet) { + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + addIf( + element = WalletNotification.Critical.BackupError, + condition = !backupValidator.isValid(userWallet.scanResponse.card) || userWallet.hasBackupError, + ) + addIf( element = WalletNotification.Critical.DevCard, condition = !cardTypesResolver.isReleaseFirmwareType(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index e7c5e885c4..da66d2d54c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -27,15 +27,20 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ) { - object DevCard : Critical( + data object DevCard : Critical( title = resourceReference(id = R.string.warning_developer_card_title), subtitle = resourceReference(id = R.string.warning_developer_card_message), ) - object FailedCardValidation : Critical( + data object FailedCardValidation : Critical( title = resourceReference(id = R.string.warning_failed_to_verify_card_title), subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message), ) + + data object BackupError : Critical( + title = resourceReference(R.string.warning_backup_errors_title), + subtitle = resourceReference(R.string.warning_backup_errors_message), + ) } sealed class Warning( From 7d9f1f0cf4baed73dd8d6fee52a21cacb8c6cef3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Mar 2024 22:00:59 +0800 Subject: [PATCH 027/109] Updated on 2026-08-14 --- .../tap/di/domain/SettingsDomainModule.kt | 6 +++++ .../details/ui/details/DetailsFragment.kt | 20 +++++++++++++++- .../details/ui/details/DetailsViewModel.kt | 24 ++++++++++++++++++- .../tangem/tap/features/main/MainViewModel.kt | 7 ++++++ .../feedback/DefaultFeedbackRepository.kt | 5 ++-- .../settings/DefaultSettingsRepository.kt | 19 +++++++-------- .../GetSupportFeedbackEmailUseCase.kt | 24 +++++++++++++++---- .../feedback/repository/FeedbackRepository.kt | 2 +- .../settings/DeleteDeprecatedLogsUseCase.kt | 16 +++++++++++++ .../repositories/SettingsRepository.kt | 2 +- 10 files changed, 105 insertions(+), 20 deletions(-) create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/DeleteDeprecatedLogsUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 7334227d99..2d8764fc7c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -120,4 +120,10 @@ internal object SettingsDomainModule { ): ShouldShowSwapPromoTokenUseCase { return ShouldShowSwapPromoTokenUseCase(swapPromoRepository) } + + @Provides + @ViewModelScoped + fun provideDeleteDeprecatedLogsUseCase(settingsRepository: SettingsRepository): DeleteDeprecatedLogsUseCase { + return DeleteDeprecatedLogsUseCase(settingsRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt index 57376f6c70..d050b873e5 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt @@ -5,8 +5,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.email.EmailSender import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.features.details.redux.DetailsState @@ -24,11 +27,26 @@ internal class DetailsFragment : ComposeFragment(), StoreSubscriber, private val walletsRepository: WalletsRepository, + private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, + private val getSupportFeedbackEmailUseCase: GetSupportFeedbackEmailUseCase, + private val emailSender: EmailSender, ) { var detailsScreenState: MutableState = mutableStateOf(updateState(store.state.detailsState)) @@ -138,7 +146,21 @@ internal class DetailsViewModel( private fun sendFeedback() { Analytics.send(Settings.ButtonSendFeedback()) - store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail())) + if (feedbackManagerFeatureToggles.isLocalLogsEnabled) { + mainScope.launch { + val email = getSupportFeedbackEmailUseCase() + emailSender.send( + email = EmailSender.Email( + address = email.address, + subject = email.subject, + message = email.message, + attachment = email.file, + ), + ) + } + } else { + store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail())) + } } private fun navigateToAppSettings() { diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index dc5b58c12a..6f5d289dd9 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -10,6 +10,7 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase +import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase import com.tangem.tap.features.main.model.MainScreenState import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel @@ -17,12 +18,14 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @HiltViewModel internal class MainViewModel @Inject constructor( private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase, private val listenToFlipsUseCase: ListenToFlipsUseCase, private val reduxNavController: ReduxNavController, private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase, + private val deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase, private val dispatchers: CoroutineDispatcherProvider, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -41,6 +44,10 @@ internal class MainViewModel @Inject constructor( observeFlips() displayBalancesHidingStatusToast() displayHiddenBalancesModalNotification() + + viewModelScope.launch(dispatchers.main) { + deleteDeprecatedLogsUseCase() + } } private fun updateAppCurrencies() { diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index 580c5fb4ab..6148da1827 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -59,9 +59,10 @@ internal class DefaultFeedbackRepository( override suspend fun getAppLogs(): List { return appPreferencesStore.getObjectMap(key = PreferencesKeys.APP_LOGS_KEY) .map { AppLogModel(timestamp = it.key.toLong(), message = it.value) } + .sortedBy(AppLogModel::timestamp) } - override suspend fun createLogFile(logs: List): File? { + override suspend fun createLogFile(logs: String): File? { return try { val file = File(context.filesDir, LOGS_FILE) file.delete() @@ -69,7 +70,7 @@ internal class DefaultFeedbackRepository( val stringWriter = StringWriter() - logs.forEach(stringWriter::append) + stringWriter.append(logs) val fileWriter = FileWriter(file) fileWriter.write(stringWriter.toString()) diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 308e823b90..277d4fc31f 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -44,22 +44,21 @@ internal class DefaultSettingsRepository( } } - override suspend fun deleteDeprecatedLogs() { - val threeDaysAgo = getThreeDaysAgoTimestamp() - + override suspend fun deleteDeprecatedLogs(maxSize: Int) { appPreferencesStore.editData { preferences -> val savedLogs = preferences.getObjectMap(PreferencesKeys.APP_LOGS_KEY) + var sum = 0 preferences.setObjectMap( key = PreferencesKeys.APP_LOGS_KEY, - value = savedLogs.filterKeys { it.toLong() > threeDaysAgo }, + value = savedLogs.entries + .sortedBy(Map.Entry::key) + .takeLastWhile { + sum += it.value.length + sum < maxSize + } + .associate { it.key to it.value }, ) } } - - private fun getThreeDaysAgoTimestamp(): Long = DateTime.now().minusDays(THREE_DAYS).millis - - private companion object { - const val THREE_DAYS = 3 - } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt index 150c5c0b2c..25db3a0ef6 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt @@ -81,15 +81,31 @@ class GetSupportFeedbackEmailUseCase( }.toString() } - private suspend fun getLogs(): List { - return feedbackRepository.getAppLogs().map { - val date = dateFormatter.print(DateTime(it.timestamp)) - "$date: ${it.message}\n" + private suspend fun getLogs(): String { + val builder = StringBuilder() + + var sum = 0 + val appLogs = feedbackRepository.getAppLogs() + for (i in appLogs.lastIndex downTo 0) { + val log = appLogs[i] + val date = dateFormatter.print(DateTime(log.timestamp)) + + val formattedLog = "$date: ${log.message}\n" + + sum += formattedLog.length + if (sum < GMAIL_MAX_FILE_SIZE) { + builder.insert(0, formattedLog) + } else { + break + } } + + return builder.toString() } private companion object { const val START2COIN_SUPPORT_EMAIL = "cardsupport@start2coin.com" const val TANGEM_SUPPORT_EMAIL = "support@tangem.com" + const val GMAIL_MAX_FILE_SIZE = 24_900_000 // ≈ 25 MB } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt index 246afde319..5e137df28a 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt @@ -16,5 +16,5 @@ interface FeedbackRepository { suspend fun getAppLogs(): List - suspend fun createLogFile(logs: List): File? + suspend fun createLogFile(logs: String): File? } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/DeleteDeprecatedLogsUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/DeleteDeprecatedLogsUseCase.kt new file mode 100644 index 0000000000..627d3f1124 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/DeleteDeprecatedLogsUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +class DeleteDeprecatedLogsUseCase( + private val settingsRepository: SettingsRepository, +) { + + suspend operator fun invoke() { + settingsRepository.deleteDeprecatedLogs(maxSize = MAX_LOGS_SIZE) + } + + private companion object { + const val MAX_LOGS_SIZE = 24_900_000 // ≈ 25 MB + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 48e6bb5ca2..061ee78557 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -12,5 +12,5 @@ interface SettingsRepository { suspend fun updateAppLogs(message: String) @Throws - suspend fun deleteDeprecatedLogs() + suspend fun deleteDeprecatedLogs(maxSize: Int) } \ No newline at end of file From 2720e5588c45053f390b35538ace879ea4ea5314 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 17 Mar 2024 21:59:37 +0800 Subject: [PATCH 028/109] Updated on 2026-08-14 --- .../com/tangem/tap/data/RuntimeUserWalletsStore.kt | 6 ++++++ .../datasource/local/userwallet/UserWalletsStore.kt | 2 ++ .../data/feedback/DefaultFeedbackRepository.kt | 12 ++++++++---- .../data/feedback/converters/CardInfoConverter.kt | 2 +- .../tangem/domain/feedback/FeedbackDataBuilder.kt | 8 +++++++- .../feedback/GetSupportFeedbackEmailUseCase.kt | 2 ++ .../com/tangem/domain/feedback/models/CardInfo.kt | 2 +- .../tangem/domain/feedback/models/UserWalletsInfo.kt | 6 ++++++ .../domain/feedback/repository/FeedbackRepository.kt | 7 +++---- 9 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/models/UserWalletsInfo.kt diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index 884fa65323..87b5944231 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -22,6 +22,12 @@ internal class RuntimeUserWalletsStore( ?.singleOrNull { it.walletId == key } } + override suspend fun getAllSyncOrNull(): List? { + return walletsStateHolder.userWalletsListManager + ?.userWallets + ?.firstOrNull() + } + override suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) { walletsStateHolder.userWalletsListManager?.update(userWalletId, update) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt index f3b1c56267..bfee20e460 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -9,6 +9,8 @@ interface UserWalletsStore { suspend fun getSyncOrNull(key: UserWalletId): UserWallet? + suspend fun getAllSyncOrNull(): List? + @Throws suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) } \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index 6148da1827..9f6c1aa883 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -10,10 +10,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore -import com.tangem.domain.feedback.models.AppLogModel -import com.tangem.domain.feedback.models.BlockchainInfo -import com.tangem.domain.feedback.models.CardInfo -import com.tangem.domain.feedback.models.PhoneInfo +import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.wallets.models.UserWallet import timber.log.Timber @@ -38,6 +35,13 @@ internal class DefaultFeedbackRepository( private val context: Context, ) : FeedbackRepository { + override suspend fun getUserWalletsInfo(): UserWalletsInfo { + return UserWalletsInfo( + selectedUserWalletId = getSelectedUserWallet().walletId.stringValue, + totalUserWallets = userWalletsStore.getAllSyncOrNull()?.size ?: error("No user wallets found"), + ) + } + override suspend fun getCardInfo(): CardInfo { return CardInfoConverter.convert(value = getSelectedUserWallet()) } diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt index e9a1a61019..7d16454467 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt @@ -15,13 +15,13 @@ internal object CardInfoConverter : Converter { override fun convert(value: UserWallet): CardInfo { return with(value.scanResponse) { CardInfo( - userWalletId = value.walletId.stringValue, cardId = card.cardId, firmwareVersion = card.firmwareVersion.stringValue, cardBlockchain = walletData?.blockchain, signedHashesList = card.wallets.map { CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString()) }, + isImported = value.isImported, isStart2Coin = value.scanResponse.card.isStart2Coin, ) } diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index 35740e1357..5350902e33 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -3,6 +3,7 @@ package com.tangem.domain.feedback import com.tangem.domain.feedback.models.BlockchainInfo import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.feedback.models.PhoneInfo +import com.tangem.domain.feedback.models.UserWalletsInfo import com.tangem.domain.feedback.utils.breakLine import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses @@ -10,12 +11,17 @@ internal class FeedbackDataBuilder { private val builder = StringBuilder() + fun addUserWalletsInfo(userWalletsInfo: UserWalletsInfo) { + builder.appendKeyValue("User Wallet ID", userWalletsInfo.selectedUserWalletId) + builder.appendKeyValue("Total saved wallets", userWalletsInfo.totalUserWallets.toString()) + } + fun addCardInfo(cardInfo: CardInfo) { builder.appendKeyValue("Card ID", cardInfo.cardId) builder.appendKeyValue("Firmware version", cardInfo.firmwareVersion) + builder.appendKeyValue("Imported wallet", if (cardInfo.isImported) "yes" else "no") builder.appendKeyValue("Card Blockchain", cardInfo.cardBlockchain) builder.appendSignedHashes(cardInfo.signedHashesList) - builder.appendKeyValue("User Wallet ID", cardInfo.userWalletId) } fun addBlockchainInfoList(blockchainInfoList: List) { diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt index 25db3a0ef6..6ab491c734 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt @@ -71,6 +71,8 @@ class GetSupportFeedbackEmailUseCase( skipLine() append( FeedbackDataBuilder().apply { + addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo()) + addDelimiter() addCardInfo(cardInfo) addDelimiter() addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList()) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt index 79eb5285c6..7ae84ac608 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt @@ -1,11 +1,11 @@ package com.tangem.domain.feedback.models data class CardInfo( - val userWalletId: String, val cardId: String, val firmwareVersion: String, val cardBlockchain: String?, val signedHashesList: List, + val isImported: Boolean, val isStart2Coin: Boolean, ) { diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/UserWalletsInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/UserWalletsInfo.kt new file mode 100644 index 0000000000..0438e80ae4 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/UserWalletsInfo.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.feedback.models + +data class UserWalletsInfo( + val selectedUserWalletId: String, + val totalUserWallets: Int, +) \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt index 5e137df28a..58281067c1 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt @@ -1,13 +1,12 @@ package com.tangem.domain.feedback.repository -import com.tangem.domain.feedback.models.AppLogModel -import com.tangem.domain.feedback.models.BlockchainInfo -import com.tangem.domain.feedback.models.CardInfo -import com.tangem.domain.feedback.models.PhoneInfo +import com.tangem.domain.feedback.models.* import java.io.File interface FeedbackRepository { + suspend fun getUserWalletsInfo(): UserWalletsInfo + suspend fun getCardInfo(): CardInfo suspend fun getBlockchainInfoList(): List From 7018750110d908d8267736ba2b485d27f867c5e4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Mar 2024 13:25:43 +0500 Subject: [PATCH 029/109] Updated on 2026-08-14 --- .../core/ui/extensions/BlockchainIcons.kt | 5 ++++ .../src/main/res/drawable/ic_radiant_22.xml | 18 +++++++++++++ .../src/main/res/drawable/img_radiant_22.xml | 27 +++++++++++++++++++ .../domain/common/extensions/Blockchain.kt | 4 +++ gradle/dependencies.toml | 2 +- 5 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 core/ui/src/main/res/drawable/ic_radiant_22.xml create mode 100644 core/ui/src/main/res/drawable/img_radiant_22.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index f49147879e..0854deb7f8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -57,6 +57,7 @@ fun getActiveIconRes(blockchainId: String): Int { "aurora", "aurora/test" -> R.drawable.img_aurora_22 "areon", "areon/test" -> R.drawable.img_areon_22 "pls", "pls/test" -> R.drawable.img_pls_22 + "radiant" -> R.drawable.img_radiant_22 else -> R.drawable.ic_alert_24 } } @@ -115,6 +116,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "aurora", "aurora/test" -> R.drawable.img_aurora_22 "areon", "areon/test" -> R.drawable.img_areon_22 "pls", "pls/test" -> R.drawable.img_pls_22 + "radiant" -> R.drawable.img_radiant_22 else -> R.drawable.ic_alert_24 } } @@ -170,6 +172,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "aurora-near" -> R.drawable.img_aurora_22 "areon" -> R.drawable.img_areon_22 "pls" -> R.drawable.img_pls_22 + "radiant" -> R.drawable.img_radiant_22 else -> R.drawable.ic_alert_24 } } @@ -228,6 +231,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "aurora", "aurora/test" -> R.drawable.ic_aurora_22 "areon", "areon/test" -> R.drawable.ic_areon_22 "pls", "pls/test" -> R.drawable.ic_pls_22 + "radiant" -> R.drawable.ic_radiant_22 else -> R.drawable.ic_alert_24 } } @@ -286,6 +290,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "aurora", "aurora/test" -> R.drawable.ic_aurora_22 "areon", "areon/test" -> R.drawable.ic_areon_22 "pls", "pls/test" -> R.drawable.ic_pls_22 + "radiant" -> R.drawable.ic_radiant_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_radiant_22.xml b/core/ui/src/main/res/drawable/ic_radiant_22.xml new file mode 100644 index 0000000000..53d3f81f81 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_radiant_22.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/img_radiant_22.xml b/core/ui/src/main/res/drawable/img_radiant_22.xml new file mode 100644 index 0000000000..8b5ea3383f --- /dev/null +++ b/core/ui/src/main/res/drawable/img_radiant_22.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 44e60f9690..3ee891252a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -97,6 +97,7 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "nexa/testnet" -> Blockchain.NexaTestnet // FIXME "zksync" -> Blockchain.ZkSyncEra // FIXME "zksync/testnet" -> Blockchain.ZkSyncEraTestnet // FIXME + "radiant" -> Blockchain.Radiant else -> null } } @@ -195,6 +196,7 @@ fun Blockchain.toNetworkId(): String { Blockchain.ZkSyncEraTestnet -> "zksync/testnet" // FIXME Blockchain.Nexa -> "nexa" // FIXME Blockchain.NexaTestnet -> "nexa/testnet" // FIXME + Blockchain.Radiant -> "radiant" } } @@ -261,6 +263,7 @@ fun Blockchain.toCoinId(): String { Blockchain.ZkSyncEraTestnet -> "zksync/testnet" // FIXME Blockchain.Nexa -> "nexa" // FIXME Blockchain.NexaTestnet -> "nexa/testnet" // FIXME + Blockchain.Radiant -> "radiant" } } @@ -292,4 +295,5 @@ private val excludedBlockchains = listOf( Blockchain.ZkSyncEraTestnet, Blockchain.Nexa, Blockchain.NexaTestnet, + Blockchain.Radiant, ) \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index b7e7c03a4e..a91c4340a5 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,7 +85,7 @@ leakcanary = "2.13" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-528" +tangemBlockchainSdk = "develop-537" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-332" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 39e8abe1bbee37298cb68276d5d8e92e4d56607c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Mar 2024 13:30:57 +0500 Subject: [PATCH 030/109] Updated on 2026-08-14 --- .../tangem/tap/di/domain/CardDomainModule.kt | 8 +++++ .../com/tangem/tap/domain/TangemSdkManager.kt | 13 ++++++++ .../card/DefaultDerivationsRepository.kt | 25 ++++++++++++++++ .../configs/feature_toggles_config.json | 4 +++ .../GetExtendedPublicKeyForCurrencyUseCase.kt | 30 +++++++++++++++++++ .../card/repository/DerivationsRepository.kt | 5 ++++ .../TokenDetailsFeatureToggles.kt | 5 ++++ features/tokendetails/impl/build.gradle.kts | 4 +++ .../di/TokenDetailsFeatureTogglesModule.kt | 21 +++++++++++++ .../DefaultTokenDetailsFeatureToggles.kt | 10 +++++++ .../router/DefaultTokenDetailsRouter.kt | 4 +++ .../router/InnerTokenDetailsRouter.kt | 3 ++ .../TokenDetailsSkeletonStateConverter.kt | 22 ++++++++++---- .../state/factory/TokenDetailsStateFactory.kt | 29 +++++++++++++----- .../viewmodels/TokenDetailsClickIntents.kt | 2 ++ .../viewmodels/TokenDetailsViewModel.kt | 29 +++++++++++++++++- features/wallet/impl/build.gradle.kts | 2 ++ .../com/tangem/lib/crypto/BlockchainUtils.kt | 6 ++++ 18 files changed, 208 insertions(+), 14 deletions(-) create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt create mode 100644 features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsFeatureTogglesModule.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 76aa092682..084134986d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -69,4 +69,12 @@ internal object CardDomainModule { fun provideIsNeedToBackupUseCase(walletStateHolder: WalletsStateHolder): IsNeedToBackupUseCase { return IsNeedToBackupUseCase(walletStateHolder) } + + @Provides + @ViewModelScoped + fun provideGetExtendedPublicKeyForCurrencyUseCase( + derivationsRepository: DerivationsRepository, + ): GetExtendedPublicKeyForCurrencyUseCase { + return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 94b72fcf1e..9c452259d9 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -26,6 +26,8 @@ import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.tap.derivationsFinder import com.tangem.tap.domain.tasks.product.CreateProductWalletTask import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse @@ -138,6 +140,17 @@ class TangemSdkManager( return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId) } + suspend fun deriveExtendedPublicKey( + cardId: String?, + walletPublicKey: ByteArray, + derivation: DerivationPath, + ): CompletionResult = withContext(Dispatchers.Main) { + runTaskAsyncReturnOnMain( + DeriveWalletPublicKeyTask(walletPublicKey, derivation), + cardId, + ) + } + suspend fun resetToFactorySettings( cardId: String, allowsRequestAccessCodeFromRepository: Boolean, diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index 4b2f3852b6..84d55370ed 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -1,12 +1,15 @@ package com.tangem.tap.domain.card +import com.tangem.common.CompletionResult import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.extensions.ByteArrayKey import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap @@ -51,6 +54,28 @@ internal class DefaultDerivationsRepository( error("This code should never be reached") } + override suspend fun deriveExtendedPublicKey( + userWalletId: UserWalletId, + derivation: DerivationPath, + ): ExtendedPublicKey? { + val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") + val walletCard = userWallet.scanResponse.card.wallets.firstOrNull { + UserWalletIdBuilder.scanResponse(userWallet.scanResponse).build()?.value + .contentEquals(userWallet.walletId.value) + } ?: return null + + val result = tangemSdkManager.deriveExtendedPublicKey( + cardId = null, + walletPublicKey = walletCard.publicKey, + derivation = derivation, + ) + + return when (result) { + is CompletionResult.Failure -> throw result.error + is CompletionResult.Success -> result.data + } + } + private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): Result { return runCatching(dispatchers.io) { userWalletsStore.update( diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index ce4c2d9448..8f979c41f0 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -18,5 +18,9 @@ { "name": "LOCAL_USER_LOGS_ENABLED", "version": "5.8.0" + }, + { + "name": "GENERATE_XPUB_ENABLED", + "version": "undefined" } ] diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt new file mode 100644 index 0000000000..4f107a0948 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.card + +import arrow.core.Either +import com.tangem.crypto.NetworkType +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Derivates an exteneded public key (xpub) based on blockchain hardened derivation + */ +class GetExtendedPublicKeyForCurrencyUseCase( + private val derivationsRepository: DerivationsRepository, +) { + suspend operator fun invoke( + userWalletId: UserWalletId, + derivation: Network.DerivationPath, + ): Either { + return Either.catch { + val derivationPath = requireNotNull(derivation.value?.let { DerivationPath(it) }) { + error("Derivation is null") + } + val hardenedNodes = derivationPath.nodes.filter { it.isHardened } + val hardenedDerivation = DerivationPath(hardenedNodes) + derivationsRepository.deriveExtendedPublicKey(userWalletId, hardenedDerivation) + ?.serialize(NetworkType.Mainnet).orEmpty() + } + } +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt index cd1af0a789..209b68817a 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt @@ -1,5 +1,7 @@ package com.tangem.domain.card.repository +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId @@ -7,4 +9,7 @@ interface DerivationsRepository { @Throws suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) + + @Throws + suspend fun deriveExtendedPublicKey(userWalletId: UserWalletId, derivation: DerivationPath): ExtendedPublicKey? } \ No newline at end of file diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt new file mode 100644 index 0000000000..d64095a963 --- /dev/null +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.tokendetails.featuretoggles + +interface TokenDetailsFeatureToggles { + fun isGenerateXPubEnabled(): Boolean +} \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 81f6868424..6db0120517 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -53,10 +53,14 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.deepLinks) implementation(projects.core.deepLinks.global) + implementation(projects.core.featuretoggles) + + implementation(projects.libs.crypto) /** Domain modules */ implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.card) implementation(projects.domain.demo) implementation(projects.domain.legacy) implementation(projects.domain.models) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsFeatureTogglesModule.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsFeatureTogglesModule.kt new file mode 100644 index 0000000000..21b9d911cb --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsFeatureTogglesModule.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.tokendetails.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.feature.tokendetails.featuretoggles.DefaultTokenDetailsFeatureToggles +import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TokenDetailsFeatureTogglesModule { + + @Provides + @Singleton + fun provideTokenDetailsFeatureToggles(featureTogglesManager: FeatureTogglesManager): TokenDetailsFeatureToggles { + return DefaultTokenDetailsFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt new file mode 100644 index 0000000000..1df3fcf7be --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.tokendetails.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles + +internal class DefaultTokenDetailsFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : TokenDetailsFeatureToggles { + override fun isGenerateXPubEnabled() = featureTogglesManager.isFeatureEnabled(name = "GENERATE_XPUB_ENABLED") +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt index 00693c486c..cadf3b9549 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt @@ -24,6 +24,10 @@ internal class DefaultTokenDetailsRouter( reduxNavController.navigate(NavigationAction.OpenUrl(url = url)) } + override fun share(text: String) { + reduxNavController.navigate(NavigationAction.Share(text)) + } + override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { reduxNavController.navigate( action = NavigationAction.NavigateTo( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt index 0c870f833a..56eead22a5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt @@ -12,5 +12,8 @@ internal interface InnerTokenDetailsRouter : TokenDetailsRouter { /** Open website by [url] */ fun openUrl(url: String) + /** Share [text] */ + fun share(text: String) + fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 30d410450e..dbc7ff632b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -5,20 +5,25 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R +import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, + private val featureToggles: TokenDetailsFeatureToggles, ) : Converter { private val iconStateConverter by lazy { TokenDetailsIconStateConverter() } @@ -27,7 +32,7 @@ internal class TokenDetailsSkeletonStateConverter( return TokenDetailsState( topAppBarConfig = TokenDetailsTopAppBarConfig( onBackClick = clickIntents::onBackClick, - tokenDetailsAppBarMenuConfig = createMenu(), + tokenDetailsAppBarMenuConfig = createMenu(value), ), tokenInfoBlockState = TokenInfoBlockState( name = value.name, @@ -60,14 +65,21 @@ internal class TokenDetailsSkeletonStateConverter( ) } - private fun createMenu(): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( - items = persistentListOf( + private fun createMenu(cryptoCurrency: CryptoCurrency): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( + items = buildList { + if (featureToggles.isGenerateXPubEnabled() && isBitcoin(cryptoCurrency.network.id.value)) { + TokenDetailsAppBarMenuConfig.MenuItem( + title = resourceReference(R.string.token_details_generate_xpub), + textColorProvider = { TangemTheme.colors.text.primary1 }, + onClick = clickIntents::onGenerateExtendedKey, + ).let(::add) + } TokenDetailsAppBarMenuConfig.MenuItem( title = TextReference.Res(id = R.string.token_details_hide_token), textColorProvider = { TangemTheme.colors.text.warning }, onClick = clickIntents::onHideClick, - ), - ), + ).let(::add) + }.toImmutableList(), ) private fun createButtons(): ImmutableList { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index c0e14ea621..06b0c838c7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -11,6 +11,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver @@ -32,9 +33,9 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.t import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter.TokenDetailsLoadingTxHistoryModel import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -44,12 +45,16 @@ internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, private val clickIntents: TokenDetailsClickIntents, + private val featureToggles: TokenDetailsFeatureToggles, symbol: String, decimals: Int, ) { private val skeletonStateConverter by lazy { - TokenDetailsSkeletonStateConverter(clickIntents = clickIntents) + TokenDetailsSkeletonStateConverter( + clickIntents = clickIntents, + featureToggles = featureToggles, + ) } private val notificationConverter by lazy { @@ -282,12 +287,12 @@ internal class TokenDetailsStateFactory( ) } - fun getStateWithUpdatedMenu(cardTypesResolver: CardTypesResolver): TokenDetailsState { + fun getStateWithUpdatedMenu(cardTypesResolver: CardTypesResolver, isBitcoin: Boolean): TokenDetailsState { return with(currentStateProvider()) { copy( topAppBarConfig = topAppBarConfig.copy( tokenDetailsAppBarMenuConfig = topAppBarConfig.tokenDetailsAppBarMenuConfig - ?.updateMenu(cardTypesResolver), + ?.updateMenu(cardTypesResolver, isBitcoin), ), ) } @@ -295,17 +300,25 @@ internal class TokenDetailsStateFactory( private fun TokenDetailsAppBarMenuConfig.updateMenu( cardTypesResolver: CardTypesResolver, + isBitcoin: Boolean, ): TokenDetailsAppBarMenuConfig? { if (cardTypesResolver.isSingleWalletWithToken()) return null - + val showGenerateExtendedKey = featureToggles.isGenerateXPubEnabled() && isBitcoin return copy( - items = persistentListOf( + items = buildList { + if (showGenerateExtendedKey) { + TokenDetailsAppBarMenuConfig.MenuItem( + title = resourceReference(R.string.token_details_generate_xpub), + textColorProvider = { TangemTheme.colors.text.primary1 }, + onClick = clickIntents::onGenerateExtendedKey, + ).let(::add) + } TokenDetailsAppBarMenuConfig.MenuItem( title = TextReference.Res(id = R.string.token_details_hide_token), textColorProvider = { TangemTheme.colors.text.warning }, onClick = clickIntents::onHideClick, - ), - ), + ).let(::add) + }.toImmutableList(), ) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index d049e3bf31..712afdeb80 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -47,4 +47,6 @@ interface TokenDetailsClickIntents { fun onSwapPromoDismiss() fun onSwapPromoClick() + + fun onGenerateExtendedKey() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index a8a0978de4..f4c38aca5c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -18,6 +18,7 @@ import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.card.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder @@ -50,8 +51,10 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTrans import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig +import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.features.tokendetails.navigation.TokenDetailsRouter +import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel @@ -85,6 +88,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, + private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase, private val swapRepository: SwapRepository, private val swapTransactionRepository: SwapTransactionRepository, private val quotesRepository: QuotesRepository, @@ -92,6 +96,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val isDemoCardUseCase: IsDemoCardUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, + featureToggles: TokenDetailsFeatureToggles, deepLinksRegistry: DeepLinksRegistry, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { @@ -121,6 +126,7 @@ internal class TokenDetailsViewModel @Inject constructor( clickIntents = this, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, + featureToggles = featureToggles, ) private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { @@ -359,7 +365,10 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } - uiState = stateFactory.getStateWithUpdatedMenu(wallet.scanResponse.cardTypesResolver) + uiState = stateFactory.getStateWithUpdatedMenu( + cardTypesResolver = wallet.scanResponse.cardTypesResolver, + isBitcoin = isBitcoin(cryptoCurrency.network.id.value), + ) } } @@ -496,6 +505,24 @@ internal class TokenDetailsViewModel @Inject constructor( } } + override fun onGenerateExtendedKey() { + viewModelScope.launch(dispatchers.main) { + val extendedKey = getExtendedPublicKeyForCurrencyUseCase( + userWalletId, + cryptoCurrency.network.derivationPath, + ).fold( + ifLeft = { + Timber.e(it.cause?.localizedMessage.orEmpty()) + "" + }, + ifRight = { it }, + ) + if (extendedKey.isNotBlank()) { + router.share(extendedKey) + } + } + } + override fun onSellClick() { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonSell(cryptoCurrency.symbol)) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index db4d102187..6b46d5c5dc 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -56,6 +56,8 @@ dependencies { implementation(projects.core.deepLinks) implementation(projects.core.deepLinks.global) + implementation(projects.libs.crypto) + /** Domain modules */ implementation(projects.domain.card) implementation(projects.domain.demo) diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 1e7a2e6a6d..a6e7acf14d 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -26,4 +26,10 @@ object BlockchainUtils { null } } + + /** If current [networkId] is Bitcoin */ + fun isBitcoin(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet + } } \ No newline at end of file From a45295cb9d1d91e9caf607d94ca7e315c7687ba9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Mar 2024 15:29:22 +0000 Subject: [PATCH 031/109] Updated on 2026-08-14 --- .../main/res/layout/fragment_disclaimer.xml | 4 +-- .../com/tangem/core/ui/extensions/Fragment.kt | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/fragment_disclaimer.xml b/app/src/main/res/layout/fragment_disclaimer.xml index 1c237b7938..346cab4f03 100644 --- a/app/src/main/res/layout/fragment_disclaimer.xml +++ b/app/src/main/res/layout/fragment_disclaimer.xml @@ -40,7 +40,7 @@ android:layout_height="match_parent" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:layout_marginBottom="90dp" + android:layout_marginBottom="74dp" app:layout_constraintBottom_toTopOf="@id/btn_accept" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -49,7 +49,7 @@ = 200) { + rtnValue = true + } + return rtnValue } \ No newline at end of file From 32884bd7430bd9be821434cded6090b2cb5fc5c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Mar 2024 13:32:17 +0000 Subject: [PATCH 032/109] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt | 2 +- .../java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 9c452259d9..548f91c4ad 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -277,7 +277,7 @@ class TangemSdkManager( allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(), maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33), batchIdFilter = CardFilter.Companion.ItemFilter.Deny( - items = setOf("0027", "0030", "0031", "0035", "DA88"), + items = setOf("0027", "0030", "0031", "0035"), ), ), ) diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt index d37487c40e..d8427a907f 100644 --- a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt +++ b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt @@ -43,7 +43,7 @@ internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, C allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(), maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33), batchIdFilter = CardFilter.Companion.ItemFilter.Deny( - items = setOf("0027", "0030", "0031", "0035", "DA88"), + items = setOf("0027", "0030", "0031", "0035"), ), ), ) From b28720df71faddfe1552233d5979e367ab0f812a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Mar 2024 17:56:30 +0800 Subject: [PATCH 033/109] Updated on 2026-08-14 --- .../paging/TxHistoryPagingSource.kt | 12 +--------- .../DefaultWalletManagersFacade.kt | 23 ++++++++++--------- .../walletmanager/WalletManagersFacade.kt | 8 ++----- 3 files changed, 15 insertions(+), 28 deletions(-) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt index 2709c26dfc..d534e238ec 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt @@ -2,10 +2,8 @@ package com.tangem.data.txhistory.repository.paging import androidx.paging.PagingSource import androidx.paging.PagingState -import com.tangem.blockchain.common.Blockchain import com.tangem.data.common.cache.CacheRegistry import com.tangem.datasource.local.txhistory.TxHistoryItemsStore -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.txhistory.models.Page import com.tangem.domain.txhistory.models.PaginationWrapper @@ -82,17 +80,9 @@ internal class TxHistoryPagingSource( } private suspend fun PaginationWrapper.addRecentTransactions(): PaginationWrapper { - val blockchain = Blockchain.fromNetworkId(sourceParams.currency.network.backendId) - - if (blockchain == null) { - Timber.e("Blockchain not found") - return this - } - val recentItems = walletManagersFacade.getRecentTransactions( userWalletId = sourceParams.userWalletId, - blockchain = blockchain, - derivationPath = sourceParams.currency.network.derivationPath.value, + currency = sourceParams.currency, ) .filterUnconfirmedTransaction() .filterIfTxAlreadyAdded(apiItems = items) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 15b93ba08e..276758748b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -520,27 +520,28 @@ class DefaultWalletManagersFacade( override suspend fun getRecentTransactions( userWalletId: UserWalletId, - blockchain: Blockchain, - derivationPath: String?, + currency: CryptoCurrency, ): List { - val walletManager = getOrCreateWalletManager( - userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = derivationPath, - ) - val feePaidCurrency = blockchain.feePaidCurrency() + val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) if (walletManager == null) { - Timber.e("Unable to get a wallet manager for blockchain: $blockchain") + Timber.e("Unable to get a wallet manager for blockchain: ${currency.network.id}") return emptyList() } val transactionDataConverter = TransactionDataToTxHistoryItemConverter( walletAddresses = SdkAddressToAddressConverter.convertList(walletManager.wallet.addresses).toSet(), - feePaidCurrency = feePaidCurrency, + feePaidCurrency = walletManager.wallet.blockchain.feePaidCurrency(), ) - return walletManager.wallet.recentTransactions.mapNotNull(transactionDataConverter::convert) + return walletManager.wallet.recentTransactions + .filter { transaction -> + when (currency) { + is CryptoCurrency.Coin -> transaction.amount.type is AmountType.Coin + is CryptoCurrency.Token -> transaction.contractAddress == currency.contractAddress + } + } + .mapNotNull(transactionDataConverter::convert) } override suspend fun tokenBalance( diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 543ef9006e..4636317765 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -227,12 +227,8 @@ interface WalletManagersFacade { network: Network, ): SimpleResult - /** Get recent transactions of [userWalletId] for [blockchain] with [derivationPath] */ - suspend fun getRecentTransactions( - userWalletId: UserWalletId, - blockchain: Blockchain, - derivationPath: String?, - ): List + /** Get recent transactions of [userWalletId] for [currency] */ + suspend fun getRecentTransactions(userWalletId: UserWalletId, currency: CryptoCurrency): List @Suppress("LongParameterList") suspend fun tokenBalance( From 4d744324028448f674b6229e299b750cfc76ab8a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Mar 2024 12:25:43 +0500 Subject: [PATCH 034/109] Updated on 2026-08-14 --- .../appbar/AppBarWithBackButtonAndIcon.kt | 2 + .../AppBarWithBackButtonAndIconContent.kt | 40 ++- .../buttons/common/TangemButtonsDefaults.kt | 2 +- .../ui/components/fields/AmountTextField.kt | 103 ++++--- features/send/impl/build.gradle.kts | 1 + .../presentation/state/SendStateFactory.kt | 32 +-- .../impl/presentation/state/SendUiState.kt | 1 + .../amount/SendAmountSubtractConverter.kt | 45 +++ .../state/fields/SendAmountFieldConverter.kt | 13 +- .../presentation/ui/SendNavigationButtons.kt | 256 +++++++++-------- .../send/impl/presentation/ui/SendScreen.kt | 35 ++- .../ui/amount/AmountFieldContainer.kt | 6 +- .../impl/presentation/ui/send/AmountBlock.kt | 78 ++++++ .../impl/presentation/ui/send/FeeBlock.kt | 96 +++++++ .../presentation/ui/send/RecipientBlock.kt | 91 ++++++ .../impl/presentation/ui/send/SendContent.kt | 260 +++++------------- .../presentation/viewmodel/SendViewModel.kt | 3 +- .../main/res/drawable/send_hint_shape_12.xml | 9 + 18 files changed, 680 insertions(+), 393 deletions(-) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt create mode 100644 features/send/impl/src/main/res/drawable/send_hint_shape_12.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt index c9e0526b4e..405217433f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt @@ -18,6 +18,7 @@ fun AppBarWithBackButtonAndIcon( onBackClick: () -> Unit, modifier: Modifier = Modifier, text: String? = null, + subtitle: String? = null, @DrawableRes backIconRes: Int? = null, @DrawableRes iconRes: Int? = null, onIconClick: (() -> Unit)? = null, @@ -27,6 +28,7 @@ fun AppBarWithBackButtonAndIcon( onBackClick = onBackClick, modifier = modifier, text = text, + subtitle = subtitle, backIconRes = backIconRes, backgroundColor = backgroundColor, iconContent = { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt index 6402068907..229fbe444c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt @@ -1,10 +1,10 @@ package com.tangem.core.ui.components.appbar import androidx.annotation.DrawableRes -import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -37,6 +37,7 @@ fun AppBarWithBackButtonAndIconContent( onBackClick: () -> Unit, modifier: Modifier = Modifier, text: String? = null, + subtitle: String? = null, @DrawableRes backIconRes: Int? = null, backIconTint: Color = TangemTheme.colors.icon.primary1, backgroundColor: Color = TangemTheme.colors.background.secondary, @@ -46,7 +47,7 @@ fun AppBarWithBackButtonAndIconContent( modifier = modifier .background(color = backgroundColor) .fillMaxWidth() - .padding(all = TangemTheme.dimens.spacing16), + .padding(horizontal = TangemTheme.dimens.spacing16), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), verticalAlignment = Alignment.CenterVertically, ) { @@ -54,6 +55,7 @@ fun AppBarWithBackButtonAndIconContent( painter = painterResource(backIconRes ?: R.drawable.ic_back_24), contentDescription = null, modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing16) .size(size = TangemTheme.dimens.size24) .clickable( interactionSource = remember { MutableInteractionSource() }, @@ -61,20 +63,37 @@ fun AppBarWithBackButtonAndIconContent( ) { onBackClick() }, tint = backIconTint, ) - AnimatedContent( - targetState = text, - modifier = Modifier.weight(1f), - transitionSpec = { fadeIn().togetherWith(fadeOut()) }, - label = "Toolbar title change", + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier.weight(1f) + .animateContentSize(), ) { - if (!it.isNullOrBlank()) { + AnimatedVisibility( + visible = !text.isNullOrBlank(), + enter = fadeIn(), + exit = fadeOut(), + label = "Toolbar title change", + ) { Text( - text = it, + text = text.orEmpty(), color = TangemTheme.colors.text.primary1, maxLines = 1, style = TangemTheme.typography.subtitle1, ) } + AnimatedVisibility( + visible = !subtitle.isNullOrBlank(), + enter = fadeIn(), + exit = fadeOut(), + label = "Toolbar subtitle change", + ) { + Text( + text = subtitle.orEmpty(), + color = TangemTheme.colors.text.secondary, + maxLines = 1, + style = TangemTheme.typography.caption2, + ) + } } iconContent() } @@ -111,6 +130,7 @@ private fun PreviewAppBarWithBackButtonAndIconInDarkTheme() { TangemTheme(isDark = true) { AppBarWithBackButtonAndIconContent( text = "Title", + subtitle = "Subtitle", onBackClick = {}, iconContent = { Row { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt index db255b710f..5a555dfcf7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color import com.tangem.core.ui.res.TangemTheme -internal object TangemButtonsDefaults { +object TangemButtonsDefaults { val elevation: ButtonElevation @Composable diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index fc0ee052f3..fee185d0ab 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -1,7 +1,9 @@ package com.tangem.core.ui.components.fields +import androidx.annotation.FloatRange import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -12,7 +14,12 @@ import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Alignment.Companion.TopStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ParagraphIntrinsics import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.createFontFamilyResolver import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection @@ -57,51 +64,77 @@ fun AmountTextField( keyboardType = KeyboardType.Number, ), keyboardActions: KeyboardActions = KeyboardActions.Default, + isAutoResize: Boolean = false, + @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) + reduceFactor: Double = 0.9, ) { val decimalFormat = rememberDecimalFormat() - + val visualTransformation = remember { AmountVisualTransformation(decimals, symbol, decimalFormat) } val placeholderTextAlign = if (placeholderAlignment == TopCenter) { TextAlign.Center } else { TextAlign.Start } - SimpleTextField( - value = value, - onValueChange = { newText -> - if (decimalFormat.isValidSymbols(newText)) { - val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals) - onValueChange(trimmed) + BoxWithConstraints(modifier = modifier) { + var fontSize = textStyle.fontSize + if (isAutoResize) { + val calculateIntrinsics = @Composable { + val transformedText = visualTransformation.filter(AnnotatedString(value)).text.text + ParagraphIntrinsics( + text = transformedText, + style = textStyle.copy(fontSize = fontSize), + density = LocalDensity.current, + fontFamilyResolver = createFontFamilyResolver(LocalContext.current), + ) } - }, - modifier = modifier - .background(TangemTheme.colors.background.action), - textStyle = textStyle.copy(textDirection = TextDirection.ContentOrLtr), - color = color, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - singleLine = true, - visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat), - decorationBox = { innerTextField -> - Box { - if (value.isBlank() && showPlaceholder) { - val placeholder = if (symbol != null) { - decimalFormat.defaultFormat().plus(" $symbol") - } else { - decimalFormat.defaultFormat() - } - Text( - text = placeholder, - style = textStyle.copy(textDirection = TextDirection.ContentOrLtr), - color = TangemTheme.colors.text.disabled, - textAlign = placeholderTextAlign, - modifier = Modifier - .align(placeholderAlignment), - ) + var intrinsics = calculateIntrinsics() + with(LocalDensity.current) { + while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) { + fontSize *= reduceFactor + intrinsics = calculateIntrinsics() } - innerTextField() } - }, - ) + } + SimpleTextField( + value = value, + onValueChange = { newText -> + if (decimalFormat.isValidSymbols(newText)) { + val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals) + onValueChange(trimmed) + } + }, + textStyle = textStyle.copy( + fontSize = fontSize, + textDirection = TextDirection.ContentOrLtr, + textAlign = TextAlign.Center, + ), + color = color, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = true, + visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat), + modifier = Modifier.background(TangemTheme.colors.background.action), + decorationBox = { innerTextField -> + Box { + if (value.isBlank() && showPlaceholder) { + val placeholder = if (symbol != null) { + decimalFormat.defaultFormat().plus(" $symbol") + } else { + decimalFormat.defaultFormat() + } + Text( + text = placeholder, + style = textStyle.copy(textDirection = TextDirection.ContentOrLtr), + color = TangemTheme.colors.text.disabled, + textAlign = placeholderTextAlign, + modifier = Modifier.align(placeholderAlignment), + ) + } + innerTextField() + } + }, + ) + } } private fun DecimalFormat.isValidSymbols(text: String): Boolean { diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index eb9e87bdf6..db20b4afc2 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { /** Compose */ implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.material3) + implementation(deps.compose.material) implementation(deps.compose.foundation) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 9b523c44f2..5f865ed61a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryItem @@ -14,6 +13,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.domain.AvailableWallet +import com.tangem.features.send.impl.presentation.state.amount.SendAmountSubtractConverter import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter @@ -45,7 +45,12 @@ internal class SendStateFactory( appCurrencyProvider = appCurrencyProvider, ) } - + private val amountSubtractConverter by lazy(LazyThreadSafetyMode.NONE) { + SendAmountSubtractConverter( + currentStateProvider = currentStateProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) { SendAmountStateConverter( appCurrencyProvider = appCurrencyProvider, @@ -81,6 +86,7 @@ internal class SendStateFactory( event = consumedEvent(), isEditingDisabled = false, isBalanceHidden = false, + cryptoCurrencySymbol = "", ) fun getReadyState(): SendUiState { @@ -90,6 +96,7 @@ internal class SendStateFactory( recipientState = state.recipientState ?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)), feeState = state.feeState ?: feeStateConverter.convert(Unit), + cryptoCurrencySymbol = cryptoCurrencyStatusProvider().currency.symbol, ) } @@ -101,6 +108,7 @@ internal class SendStateFactory( ?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)), feeState = state.feeState ?: feeStateConverter.convert(Unit), isEditingDisabled = true, + cryptoCurrencySymbol = cryptoCurrencyStatusProvider().currency.symbol, ) } @@ -218,22 +226,12 @@ internal class SendStateFactory( //endregion //region send - fun onSubtractSelect(isSubtract: Boolean, isAmountSubtractAvailable: Boolean): SendUiState { + fun onSubtractSelect(isAmountSubtractAvailable: Boolean): SendUiState { val state = currentStateProvider() - val fee = state.feeState?.fee ?: return state - val amountState = state.amountState ?: return state - val amount = amountState.amountTextField.cryptoAmount - val amountValue = amount.value ?: return state - val amountToSend = if (isSubtract && isAmountSubtractAvailable) { - val feeValue = fee.amount.value ?: return state - amountValue.minus(feeValue) - } else { - amountValue - } - return state.copy( - amountState = amountStateConverter.convert(amountToSend.parseBigDecimal(amount.decimals)), - sendState = state.sendState.copy(isSubtract = isSubtract), - ) + + if (!isAmountSubtractAvailable) return state + + return amountSubtractConverter.convert(Unit) } fun getSendingStateUpdate(isSending: Boolean): SendUiState { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index 7572a37ae9..00d7a9dbbe 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -25,6 +25,7 @@ import java.math.BigDecimal internal data class SendUiState( val clickIntents: SendClickIntents, val isEditingDisabled: Boolean, + val cryptoCurrencySymbol: String, val amountState: SendStates.AmountState? = null, val recipientState: SendStates.RecipientState? = null, val feeState: SendStates.FeeState? = null, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt new file mode 100644 index 0000000000..4d00bd9b2e --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt @@ -0,0 +1,45 @@ +package com.tangem.features.send.impl.presentation.state.amount + +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class SendAmountSubtractConverter( + private val currentStateProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + override fun convert(value: Unit): SendUiState { + val state = currentStateProvider() + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val amountState = state.amountState ?: return state + val feeValue = state.feeState?.fee?.amount?.value ?: return state + val amountTextField = amountState.amountTextField + val amountValue = amountTextField.cryptoAmount.value ?: return state + val fiatRate = cryptoCurrencyStatus.value.fiatRate + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + + val decimalCryptoValue = amountValue.minus(feeValue) + + if (decimalCryptoValue < BigDecimal.ZERO) return state + + val decimalFiatValue = decimalCryptoValue.multiply(fiatRate) + val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals) + val fiatValue = decimalFiatValue.parseBigDecimal(fiatDecimals) + + return state.copy( + sendState = state.sendState.copy(isSubtract = true), + amountState = amountState.copy( + amountTextField = amountTextField.copy( + value = cryptoValue, + fiatValue = fiatValue, + cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt index 37bdcf2eae..332a736cd0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt @@ -32,11 +32,12 @@ internal class SendAmountFieldConverter( val cryptoDecimal = value.toBigDecimalOrDefault() val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency) val fiatRate = cryptoCurrencyStatus.value.fiatRate - val fiatValue = if (value.isEmpty()) { - "" + val (fiatValue, fiatDecimal) = if (value.isEmpty()) { + "" to BigDecimal.ZERO } else { val fiatDecimal = fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO - fiatDecimal.parseBigDecimal(FIAT_DECIMALS) + val fiatValue = fiatDecimal.parseBigDecimal(FIAT_DECIMALS) + fiatValue to fiatDecimal } val isDoneActionEnabled = !cryptoDecimal.isZero() return SendTextField.AmountField( @@ -50,16 +51,16 @@ internal class SendAmountFieldConverter( keyboardActions = KeyboardActions(onDone = { clickIntents.onNextClick() }), isFiatValue = false, cryptoAmount = cryptoAmount, - fiatAmount = getAppCurrencyAmount(appCurrencyProvider()), + fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()), isError = false, error = TextReference.Res(R.string.swapping_insufficient_funds), isFiatUnavailable = fiatRate == null, ) } - private fun getAppCurrencyAmount(appCurrency: AppCurrency) = Amount( + private fun getAppCurrencyAmount(fiatValue: BigDecimal, appCurrency: AppCurrency) = Amount( currencySymbol = appCurrency.symbol, - value = BigDecimal.ZERO, + value = fiatValue, decimals = FIAT_DECIMALS, type = AmountType.FiatType(appCurrency.code), ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 5c0dfc787b..0344343e70 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -1,18 +1,12 @@ package com.tangem.features.send.impl.presentation.ui -import androidx.annotation.StringRes -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandHorizontally -import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -22,161 +16,185 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.R -import com.tangem.core.ui.components.* -import com.tangem.core.ui.extensions.rememberHapticFeedback +import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType @Composable internal fun SendNavigationButtons(uiState: SendUiState, currentState: SendUiCurrentScreen) { - Row( + val isSuccess = uiState.sendState.isSuccess + val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess + val isSentState = currentState.type == SendUiStateType.Send && isSuccess + Column( modifier = Modifier - .fillMaxWidth() - .padding(bottom = TangemTheme.dimens.spacing12), + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), ) { - SendSecondaryNavigationButton( - uiState = uiState, - currentState = currentState, + SendingText(uiState = uiState, isVisible = isSendingState) + SendDoneButtons( + txUrl = uiState.sendState.txUrl, + onExploreClick = uiState.clickIntents::onExploreClick, + onShareClick = uiState.clickIntents::onShareClick, + isVisible = isSentState, ) - SendPrimaryNavigationButton( + SendNavigationButton( uiState = uiState, currentState = currentState, - modifier = Modifier - .weight(1f) - .padding(horizontal = TangemTheme.dimens.spacing16), + modifier = Modifier, ) } } @Composable -private fun SendSecondaryNavigationButton(uiState: SendUiState, currentState: SendUiCurrentScreen) { - val isEditingDisabled = uiState.isEditingDisabled - val isFromConfirmation = currentState.isFromConfirmation - val isCorrectScreen = currentState.type == SendUiStateType.Amount || currentState.type == SendUiStateType.Fee - AnimatedVisibility( - visible = !isEditingDisabled && isCorrectScreen && !isFromConfirmation, - enter = expandHorizontally(expandFrom = Alignment.End), - exit = shrinkHorizontally(shrinkTowards = Alignment.End), - ) { - Icon( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing16) - .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.button.secondary) - .clickable { - uiState.clickIntents.onPrevClick() - } - .padding(TangemTheme.dimens.spacing12), - painter = painterResource(R.drawable.ic_back_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - } -} - -@Composable -private fun SendPrimaryNavigationButton( +private fun SendNavigationButton( uiState: SendUiState, currentState: SendUiCurrentScreen, modifier: Modifier = Modifier, ) { + val hapticFeedback = LocalHapticFeedback.current + val isEditingDisabled = uiState.isEditingDisabled val isSuccess = uiState.sendState.isSuccess - val isSending = uiState.sendState.isSending - val txUrl = uiState.sendState.txUrl + + val isFromConfirmation = currentState.isFromConfirmation + val isCorrectScreen = currentState.type == SendUiStateType.Amount || currentState.type == SendUiStateType.Fee + val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess val (buttonTextId, buttonClick) = getButtonData( currentState = currentState, isSuccess = isSuccess, uiState = uiState, ) - - val isButtonEnabled = isButtonEnabled( - currentState = currentState, - uiState = uiState, - ) - - AnimatedContent( - targetState = buttonTextId, - label = "Update send screen state", + val isButtonEnabled = isButtonEnabled(currentState, uiState) + val buttonIcon = if (isSendingState) { + TangemButtonIconPosition.End(R.drawable.ic_tangem_24) + } else { + TangemButtonIconPosition.None + } + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), modifier = modifier, - ) { textId -> - when { - currentState.type == SendUiStateType.Send && !isSuccess -> { - val hapticFeedback = rememberHapticFeedback(state = currentState, onAction = buttonClick) - PrimaryButtonIconEnd( - text = stringResource(textId), - iconResId = R.drawable.ic_tangem_24, - enabled = isButtonEnabled, - onClick = hapticFeedback, - showProgress = isSending, - ) - } - currentState.type == SendUiStateType.Send && isSuccess -> { - PrimaryButtonsDone( - textRes = textId, - txUrl = txUrl, - onExploreClick = uiState.clickIntents::onExploreClick, - onShareClick = uiState.clickIntents::onShareClick, - onDoneClick = buttonClick, - modifier = Modifier, - ) - } - else -> { - PrimaryButton( - text = stringResource(textId), - enabled = isButtonEnabled, - onClick = buttonClick, - ) - } + ) { + AnimatedVisibility( + visible = !isEditingDisabled && isCorrectScreen && !isFromConfirmation, + enter = expandHorizontally(expandFrom = Alignment.End), + exit = shrinkHorizontally(shrinkTowards = Alignment.End), + ) { + Icon( + painter = painterResource(R.drawable.ic_back_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.button.secondary) + .clickable { uiState.clickIntents.onPrevClick() } + .padding(TangemTheme.dimens.spacing12), + ) + } + TangemButton( + text = stringResource(buttonTextId), + icon = buttonIcon, + enabled = isButtonEnabled, + onClick = { + if (isSendingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + buttonClick() + }, + showProgress = uiState.sendState.isSending, + modifier = Modifier.fillMaxWidth(), + colors = TangemButtonsDefaults.primaryButtonColors, + ) + } +} + +@Composable +private fun SendingText(uiState: SendUiState, isVisible: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility( + visible = isVisible, + modifier = modifier, + enter = slideInVertically().plus(fadeIn()), + exit = slideOutVertically().plus(fadeOut()), + label = "Animate show sending state text", + ) { + val amountState = uiState.amountState + val feeState = uiState.feeState + val fiatRate = feeState?.rate + val fiatAmount = amountState?.amountTextField?.fiatAmount + val feeFiat = feeState?.fee?.amount?.value?.multiply(fiatRate) + val sendingFiat = feeFiat?.let { fiatAmount?.value?.plus(it) } + + if (feeFiat != null && sendingFiat != null) { + val sendingValue = BigDecimalFormatter.formatFiatAmount( + fiatAmount = sendingFiat, + fiatCurrencyCode = feeState.appCurrency.code, + fiatCurrencySymbol = feeState.appCurrency.symbol, + ) + val feeValue = BigDecimalFormatter.formatFiatAmount( + fiatAmount = feeFiat, + fiatCurrencyCode = feeState.appCurrency.code, + fiatCurrencySymbol = feeState.appCurrency.symbol, + ) + Text( + text = stringResource(id = R.string.send_summary_transaction_description, sendingValue, feeValue), + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + ) } } } @Composable -private fun PrimaryButtonsDone( - @StringRes textRes: Int, +private fun SendDoneButtons( txUrl: String, onExploreClick: () -> Unit, onShareClick: () -> Unit, - onDoneClick: () -> Unit, + isVisible: Boolean, modifier: Modifier = Modifier, ) { val hapticFeedback = LocalHapticFeedback.current val context = LocalContext.current - Column(modifier = modifier) { - if (txUrl.isNotBlank()) { - Row { - SecondaryButtonIconStart( - text = stringResource(id = R.string.common_explore), - iconResId = R.drawable.ic_web_24, - onClick = onExploreClick, - modifier = Modifier.weight(1f), - ) - SpacerW12() - SecondaryButtonIconStart( - text = stringResource(id = R.string.common_share), - iconResId = R.drawable.ic_share_24, - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - context.shareText(txUrl) - onShareClick() - }, - modifier = Modifier.weight(1f), - ) - } - SpacerH12() + AnimatedVisibility( + visible = isVisible && txUrl.isNotBlank(), + modifier = modifier, + enter = slideInVertically().plus(fadeIn()), + exit = slideOutVertically().plus(fadeOut()), + label = "Animate show sent state buttons", + ) { + Row(modifier = Modifier.padding(TangemTheme.dimens.spacing12)) { + SecondaryButtonIconStart( + text = stringResource(id = R.string.common_explore), + iconResId = R.drawable.ic_web_24, + onClick = onExploreClick, + modifier = Modifier.weight(1f), + ) + SpacerW12() + SecondaryButtonIconStart( + text = stringResource(id = R.string.common_share), + iconResId = R.drawable.ic_share_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + context.shareText(txUrl) + onShareClick() + }, + modifier = Modifier.weight(1f), + ) } - PrimaryButton( - text = stringResource(id = textRes), - enabled = true, - onClick = onDoneClick, - modifier = Modifier.fillMaxWidth(), - ) } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 7cd9b84daf..729c3c9a24 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -1,20 +1,21 @@ package com.tangem.features.send.impl.presentation.ui import androidx.activity.compose.BackHandler -import androidx.compose.animation.* +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.core.tween +import androidx.compose.animation.togetherWith import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.* import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen @@ -40,12 +41,22 @@ internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow R.string.send_amount_label - SendUiStateType.Recipient -> R.string.send_recipient_label - SendUiStateType.Fee -> R.string.common_fee_selector_title - SendUiStateType.Send -> if (!uiState.sendState.isSuccess) R.string.send_confirm_label else null + SendUiStateType.Amount -> resourceReference(R.string.send_amount_label) + SendUiStateType.Recipient -> resourceReference(R.string.send_recipient_label) + SendUiStateType.Fee -> resourceReference(R.string.common_fee_selector_title) + SendUiStateType.Send -> if (!uiState.sendState.isSuccess) { + resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencySymbol)) + } else { + null + } else -> null } + val isSending = currentState.value.type == SendUiStateType.Send && !uiState.sendState.isSuccess + val subtitleRes = if (isSending) { + uiState.amountState?.walletName + } else { + null + } val iconRes = if (currentState.value.type == SendUiStateType.Recipient) { R.drawable.ic_qrcode_scan_24 } else { @@ -53,12 +64,14 @@ internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow Unit, +) { + val amount = amountState.amountTextField + + val cryptoAmount = getAmountWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) + val fiatAmount = getAmountWithSymbol(amount.fiatValue, amount.fiatAmount.currencySymbol) + val backgroundColor = if (isEditingDisabled) { + TangemTheme.colors.button.disabled + } else { + TangemTheme.colors.background.action + } + + val (firstAmount, secondAmount) = if (amount.isFiatValue) { + fiatAmount to cryptoAmount + } else { + cryptoAmount to fiatAmount + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(backgroundColor) + .clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick) + .padding( + vertical = TangemTheme.dimens.spacing14, + horizontal = TangemTheme.dimens.spacing16, + ), + ) { + TokenIcon(state = amountState.tokenIconState) + ResizableText( + text = firstAmount, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .padding(top = TangemTheme.dimens.spacing16), + ) + Text( + text = secondAmount, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = TangemTheme.dimens.spacing4), + ) + } +} + +private fun getAmountWithSymbol(amount: String, symbol: String): String { + return "$amount $symbol" +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt new file mode 100644 index 0000000000..13ff8cab42 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -0,0 +1,96 @@ +package com.tangem.features.send.impl.presentation.ui.send + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType + +@Composable +internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) { + val fee = feeState.fee ?: return + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return + val (title, icon) = when (feeSelectorState.selectedFee) { + FeeType.Slow -> R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24 + FeeType.Market -> R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 + FeeType.Fast -> R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24 + FeeType.Custom -> R.string.common_fee_selector_option_custom to R.drawable.ic_edit_24 + } + val feeCryptoValue = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = fee.amount.value, + cryptoCurrency = fee.amount.currencySymbol, + decimals = fee.amount.decimals, + ) + val feeFiatValue = BigDecimalFormatter.formatFiatAmount( + fiatAmount = feeState.rate?.let { fee.amount.value?.multiply(it) }, + fiatCurrencyCode = feeState.appCurrency.code, + fiatCurrencySymbol = feeState.appCurrency.symbol, + ) + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(enabled = !isSuccess) { onClick() } + .padding( + vertical = TangemTheme.dimens.spacing12, + horizontal = TangemTheme.dimens.spacing14, + ), + ) { + Text( + text = stringResource(R.string.common_network_fee_title), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + Row( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(icon), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + Text( + text = stringResource(title), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), + ) + EllipsisText( + text = feeCryptoValue, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.End, + ellipsis = TextEllipsis.OffsetEnd(fee.amount.currencySymbol.length), + modifier = Modifier + .weight(1f) + .padding(start = TangemTheme.dimens.spacing4), + ) + Text( + text = "($feeFiatValue)", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt new file mode 100644 index 0000000000..7169e38f49 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt @@ -0,0 +1,91 @@ +package com.tangem.features.send.impl.presentation.ui.send + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.fields.SendTextField + +@Composable +internal fun RecipientBlock( + recipientState: SendStates.RecipientState, + isSuccess: Boolean, + isEditingDisabled: Boolean, + onClick: () -> Unit, +) { + val backgroundColor = if (isEditingDisabled) { + TangemTheme.colors.button.disabled + } else { + TangemTheme.colors.background.action + } + + Column( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(backgroundColor) + .clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick) + .padding(TangemTheme.dimens.spacing12), + ) { + AddressBlock(recipientState.addressTextField) + MemoBlock(recipientState.memoTextField) + } +} + +@Composable +private fun AddressBlock(address: SendTextField.RecipientAddress) { + Text( + text = address.label.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) { + IdentIcon( + address = address.value, + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius18)) + .background(TangemTheme.colors.background.tertiary), + ) + Text( + text = address.value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } +} + +@Composable +private fun MemoBlock(memo: SendTextField.RecipientMemo?) { + val showMemo = memo != null && memo.value.isNotBlank() + if (showMemo) { + HorizontalDivider( + color = TangemTheme.colors.stroke.primary, + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + ) + Text( + text = memo?.label?.resolveReference().orEmpty(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + Text( + text = memo?.value.orEmpty(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 6d335ee554..6cd08ba079 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -1,235 +1,121 @@ package com.tangem.features.send.impl.presentation.ui.send -import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.withStyle -import com.tangem.common.Strings -import com.tangem.core.ui.components.inputrow.InputRowDefault -import com.tangem.core.ui.components.inputrow.InputRowImage -import com.tangem.core.ui.components.inputrow.InputRowRecipientDefault import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.transactions.TransactionDoneTitle -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount -import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount -import com.tangem.domain.tokens.model.AmountType import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendNotification -import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.SendUiState import kotlinx.collections.immutable.ImmutableList +private const val TAP_HELP_KEY = "TAP_HELP_KEY" +private const val BLOCKS_KEY = "BLOCKS_KEY" + @Suppress("LongMethod") @Composable internal fun SendContent(uiState: SendUiState) { - val amountState = uiState.amountState ?: return - val recipientState = uiState.recipientState ?: return - val feeState = uiState.feeState ?: return val sendState = uiState.sendState - - val isSuccess = sendState.isSuccess - val timestamp = sendState.transactionDate - LazyColumn( modifier = Modifier .fillMaxSize() .padding(horizontal = TangemTheme.dimens.spacing16), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - item { - Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12)) { - AnimatedVisibility(visible = isSuccess) { - TransactionDoneTitle( - titleRes = R.string.sent_transaction_sent_title, - date = timestamp, - ) - } - AnimatedVisibility(visible = !isSuccess) { - FromWallet( - walletName = amountState.walletName, - walletBalance = amountState.walletBalance.resolveReference(), - isBalanceHidden = uiState.isBalanceHidden, - ) - } - RecipientBlock( - recipientState = recipientState, - isSuccess = isSuccess, - isEditingDisabled = uiState.isEditingDisabled, - onClick = uiState.clickIntents::showRecipient, - ) - AmountBlock( - amountState = amountState, - isSuccess = isSuccess, - isEditingDisabled = uiState.isEditingDisabled, - onClick = uiState.clickIntents::showAmount, - ) - FeeBlock( - feeState = feeState, - isSuccess = isSuccess, - onClick = uiState.clickIntents::showFee, - ) - } - } + blocks(uiState) + tapHelp(isDisplay = sendState.notifications.isEmpty() && !uiState.sendState.isSuccess) notifications(sendState.notifications) } } -@Composable -private fun FromWallet(walletName: String, walletBalance: String, isBalanceHidden: Boolean) { - Column( - modifier = Modifier - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.button.disabled) - .padding(TangemTheme.dimens.spacing12), - ) { - Text( - text = buildAnnotatedString { - append(stringResource(R.string.send_from_wallet_android)) - append(" ") - withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(walletName) - } - }, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, - ) - val balance = if (isBalanceHidden) Strings.STARS else walletBalance - AnimatedContent( - targetState = balance, - label = "Hide Balance Animation", - ) { - Text( - text = it, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing8, - ), +private fun LazyListScope.blocks(uiState: SendUiState) { + val amountState = uiState.amountState ?: return + val recipientState = uiState.recipientState ?: return + val feeState = uiState.feeState ?: return + val sendState = uiState.sendState + val isSuccess = sendState.isSuccess + val timestamp = sendState.transactionDate + + item(key = BLOCKS_KEY) { + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12)) { + AnimatedVisibility( + visible = isSuccess, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + ) { + TransactionDoneTitle( + titleRes = R.string.sent_transaction_sent_title, + date = timestamp, + ) + } + RecipientBlock( + recipientState = recipientState, + isSuccess = isSuccess, + isEditingDisabled = uiState.isEditingDisabled, + onClick = uiState.clickIntents::showRecipient, + ) + AmountBlock( + amountState = amountState, + isSuccess = isSuccess, + isEditingDisabled = uiState.isEditingDisabled, + onClick = uiState.clickIntents::showAmount, + ) + FeeBlock( + feeState = feeState, + isSuccess = isSuccess, + onClick = uiState.clickIntents::showFee, ) } } } -@Composable -private fun AmountBlock( - amountState: SendStates.AmountState, - isSuccess: Boolean, - isEditingDisabled: Boolean, - onClick: () -> Unit, -) { - val amount = amountState.amountTextField - - val cryptoAmount = formatCryptoAmount( - cryptoAmount = amount.cryptoAmount.value, - cryptoCurrency = amount.cryptoAmount.currencySymbol, - decimals = amount.cryptoAmount.decimals, - ) - val fiatAmount = formatFiatAmount( - fiatAmount = amount.fiatAmount.value, - fiatCurrencyCode = (amount.fiatAmount.type as AmountType.FiatType).code, - fiatCurrencySymbol = amount.fiatAmount.currencySymbol, - ) - val backgroundColor = if (isEditingDisabled) { - TangemTheme.colors.button.disabled - } else { - TangemTheme.colors.background.action - } - InputRowImage( - title = TextReference.Res(R.string.send_amount_label), - subtitle = TextReference.Str(cryptoAmount), - caption = TextReference.Str(fiatAmount), - tokenIconState = amountState.tokenIconState, - showNetworkIcon = true, - modifier = Modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(backgroundColor) - .clickable(enabled = !isSuccess && !isEditingDisabled) { onClick() }, - ) -} - -@Composable -private fun RecipientBlock( - recipientState: SendStates.RecipientState, - isSuccess: Boolean, - isEditingDisabled: Boolean, - onClick: () -> Unit, -) { - val address = recipientState.addressTextField - val memo = recipientState.memoTextField - val backgroundColor = if (isEditingDisabled) { - TangemTheme.colors.button.disabled - } else { - TangemTheme.colors.background.action - } - - Column( - modifier = Modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(backgroundColor) - .clickable(enabled = !isSuccess && !isEditingDisabled) { onClick() }, - ) { - val showMemo = memo != null && memo.value.isNotBlank() - InputRowRecipientDefault( - title = TextReference.Res(R.string.send_recipient), - value = address.value, - showDivider = showMemo, - ) - if (showMemo) { - InputRowDefault( - title = TextReference.Res(R.string.send_extras_hint_memo), - text = TextReference.Str(memo?.value.orEmpty()), - ) +@OptIn(ExperimentalFoundationApi::class) +private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) { + if (isDisplay) { + item(key = TAP_HELP_KEY) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxWidth() + .animateItemPlacement(), + ) { + val background = TangemTheme.colors.button.secondary + Icon( + painter = painterResource(id = R.drawable.send_hint_shape_12), + tint = TangemTheme.colors.button.secondary, + contentDescription = null, + modifier = Modifier, + ) + Text( + text = stringResource(id = R.string.send_summary_tap_hint), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(background) + .padding( + horizontal = TangemTheme.dimens.spacing14, + vertical = TangemTheme.dimens.spacing12, + ), + ) + } } } } -@Composable -private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) { - val fee = feeState.fee ?: return - val feeCryptoValue = formatCryptoAmount( - cryptoAmount = fee.amount.value, - cryptoCurrency = fee.amount.currencySymbol, - decimals = fee.amount.decimals, - ) - val feeFiatValue = formatFiatAmount( - fiatAmount = feeState.rate?.let { fee.amount.value?.multiply(it) }, - fiatCurrencyCode = feeState.appCurrency.code, - fiatCurrencySymbol = feeState.appCurrency.symbol, - ) - InputRowDefault( - title = resourceReference(R.string.common_network_fee_title), - text = resourceReference( - id = R.string.send_wallet_balance_format, - wrappedList(feeCryptoValue, feeFiatValue), - ), - modifier = Modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action) - .clickable(enabled = !isSuccess) { onClick() }, - ) -} - @OptIn(ExperimentalFoundationApi::class) internal fun LazyListScope.notifications(configs: ImmutableList, modifier: Modifier = Modifier) { items( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 0617f6e83b..073fb98d89 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -466,7 +466,6 @@ internal class SendViewModel @Inject constructor( ) return } else { - uiState = stateFactory.onSubtractSelect(false, isAmountSubtractAvailable) analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false)) } if (checkIfFeeTooLow(uiState)) { @@ -606,7 +605,7 @@ internal class SendViewModel @Inject constructor( } override fun onSubtractSelect() { - uiState = stateFactory.onSubtractSelect(true, isAmountSubtractAvailable) + uiState = stateFactory.onSubtractSelect(isAmountSubtractAvailable) stateRouter.showSend() analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(true)) } diff --git a/features/send/impl/src/main/res/drawable/send_hint_shape_12.xml b/features/send/impl/src/main/res/drawable/send_hint_shape_12.xml new file mode 100644 index 0000000000..05cc9e9b5b --- /dev/null +++ b/features/send/impl/src/main/res/drawable/send_hint_shape_12.xml @@ -0,0 +1,9 @@ + + + From 36f81792358aa1b77798deac89121ae4c7662e81 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Mar 2024 19:10:15 +0500 Subject: [PATCH 035/109] Updated on 2026-08-14 --- .../tap/di/domain/SettingsDomainModule.kt | 14 +++++++ .../local/preferences/PreferencesKeys.kt | 2 + .../settings/DefaultSettingsRepository.kt | 14 +++++++ .../settings/IsSendTapHelpEnabledUseCase.kt | 13 +++++++ .../settings/NeverToShowTapHelpUseCase.kt | 13 +++++++ .../repositories/SettingsRepository.kt | 4 ++ features/send/impl/build.gradle.kts | 1 + .../state/SendNotificationFactory.kt | 4 +- .../presentation/state/SendStateFactory.kt | 30 ++++++++++++--- .../impl/presentation/state/SendUiState.kt | 20 +++++----- .../amount/SendAmountSubtractConverter.kt | 2 +- .../confirm/SendConfirmStateConverter.kt | 25 ++++++++++++ .../presentation/ui/SendNavigationButtons.kt | 12 +++--- .../send/impl/presentation/ui/SendScreen.kt | 3 +- .../impl/presentation/ui/send/SendContent.kt | 31 ++++++++++++--- .../presentation/viewmodel/SendViewModel.kt | 38 ++++++++++++++++--- 16 files changed, 189 insertions(+), 37 deletions(-) create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/IsSendTapHelpEnabledUseCase.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowTapHelpUseCase.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendConfirmStateConverter.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 2d8764fc7c..ac3b9c779e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -126,4 +126,18 @@ internal object SettingsDomainModule { fun provideDeleteDeprecatedLogsUseCase(settingsRepository: SettingsRepository): DeleteDeprecatedLogsUseCase { return DeleteDeprecatedLogsUseCase(settingsRepository) } + + @Provides + @ViewModelScoped + fun provideIsSendTapHelpPreviewEnabledUseCase( + settingsRepository: SettingsRepository, + ): IsSendTapHelpEnabledUseCase { + return IsSendTapHelpEnabledUseCase(settingsRepository = settingsRepository) + } + + @Provides + @ViewModelScoped + fun provideNeverToShowTapHelpUseCase(settingsRepository: SettingsRepository): NeverToShowTapHelpUseCase { + return NeverToShowTapHelpUseCase(settingsRepository = settingsRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 22d8aa8ea3..ab0ab7c350 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -63,6 +63,8 @@ object PreferencesKeys { val APP_LOGS_KEY by lazy { stringPreferencesKey(name = "app_logs") } + val SEND_TAP_HELP_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "sendTapHelpPreview") } + fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region") } diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 277d4fc31f..71e8f8d654 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -61,4 +61,18 @@ internal class DefaultSettingsRepository( ) } } + + override suspend fun isSendTapHelpPreviewEnabled(): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.SEND_TAP_HELP_PREVIEW_KEY, + default = true, + ) + } + + override suspend fun setSendTapHelpPreviewAvailability(isEnabled: Boolean) { + appPreferencesStore.store( + key = PreferencesKeys.SEND_TAP_HELP_PREVIEW_KEY, + value = isEnabled, + ) + } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IsSendTapHelpEnabledUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IsSendTapHelpEnabledUseCase.kt new file mode 100644 index 0000000000..1e828a0bc6 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/IsSendTapHelpEnabledUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +/** + * Checks if tap help is enabled + * + * @property settingsRepository settings repository + */ +class IsSendTapHelpEnabledUseCase(private val settingsRepository: SettingsRepository) { + + suspend operator fun invoke(): Boolean = settingsRepository.isSendTapHelpPreviewEnabled() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowTapHelpUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowTapHelpUseCase.kt new file mode 100644 index 0000000000..bf72970303 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowTapHelpUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +/** + * Never to show tap help + * + * @property settingsRepository settings repository + */ +class NeverToShowTapHelpUseCase(private val settingsRepository: SettingsRepository) { + + suspend operator fun invoke() = settingsRepository.setSendTapHelpPreviewAvailability(isEnabled = false) +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 061ee78557..63a3ab3247 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -13,4 +13,8 @@ interface SettingsRepository { @Throws suspend fun deleteDeprecatedLogs(maxSize: Int) + + suspend fun isSendTapHelpPreviewEnabled(): Boolean + + suspend fun setSendTapHelpPreviewAvailability(isEnabled: Boolean) } \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index db20b4afc2..587cb6d67c 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -72,6 +72,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.qrScanning) implementation(projects.domain.qrScanning.models) + implementation(projects.domain.settings) /** Feature modules */ implementation(projects.features.send.api) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt index 426ddab71a..0064f20684 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt @@ -39,7 +39,7 @@ internal class SendNotificationFactory( .filter { it.type == SendUiStateType.Send } .map { val state = currentStateProvider() - val sendState = state.sendState + val sendState = state.sendState ?: return@map persistentListOf() val feeState = state.feeState ?: return@map persistentListOf() val feeAmount = feeState.fee?.amount?.value ?: BigDecimal.ZERO val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO @@ -60,7 +60,7 @@ internal class SendNotificationFactory( fun dismissNotificationState(clazz: Class): SendUiState { val state = currentStateProvider() - val sendState = state.sendState + val sendState = state.sendState ?: return state val notificationsToRemove = sendState.notifications.filterIsInstance(clazz) val updatedNotifications = sendState.notifications.toMutableList() updatedNotifications.removeAll(notificationsToRemove) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 5f865ed61a..b5f1d0f72e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -13,8 +13,9 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.domain.AvailableWallet -import com.tangem.features.send.impl.presentation.state.amount.SendAmountSubtractConverter import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter +import com.tangem.features.send.impl.presentation.state.amount.SendAmountSubtractConverter +import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter @@ -33,6 +34,7 @@ internal class SendStateFactory( private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val feeCryptoCurrencyStatusProvider: Provider, + private val isTapHelpPreviewEnabledProvider: Provider, private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, ) { @@ -72,7 +74,11 @@ internal class SendStateFactory( feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) } - + private val confirmStateConverter by lazy(LazyThreadSafetyMode.NONE) { + SendConfirmStateConverter( + isTapHelpPreviewEnabledProvider = isTapHelpPreviewEnabledProvider, + ) + } private val recipientListStateConverter by lazy(LazyThreadSafetyMode.NONE) { SendRecipientListConverter( currentStateProvider = currentStateProvider, @@ -96,6 +102,7 @@ internal class SendStateFactory( recipientState = state.recipientState ?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)), feeState = state.feeState ?: feeStateConverter.convert(Unit), + sendState = confirmStateConverter.convert(Unit), cryptoCurrencySymbol = cryptoCurrencyStatusProvider().currency.symbol, ) } @@ -236,21 +243,22 @@ internal class SendStateFactory( fun getSendingStateUpdate(isSending: Boolean): SendUiState { val state = currentStateProvider() - return state.copy(sendState = state.sendState.copy(isSending = isSending)) + return state.copy(sendState = state.sendState?.copy(isSending = isSending)) } fun getTransactionSendState(txData: TransactionData): SendUiState { val state = currentStateProvider() val cryptoCurrency = cryptoCurrencyStatusProvider().currency - + val sendState = state.sendState ?: return state val txUrl = getExplorerTransactionUrlUseCase( txHash = txData.hash.orEmpty(), networkId = cryptoCurrency.network.id, ).getOrElse { "" } return state.copy( - sendState = state.sendState.copy( + sendState = sendState.copy( transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(), isSuccess = true, + showTapHelp = false, txUrl = txUrl, notifications = persistentListOf(), ), @@ -259,13 +267,23 @@ internal class SendStateFactory( fun getSendNotificationState(notifications: ImmutableList): SendUiState { val state = currentStateProvider() + val sendState = state.sendState ?: return state val hasErrorNotifications = notifications.any { it is SendNotification.Error } return state.copy( - sendState = state.sendState.copy( + sendState = sendState.copy( isPrimaryButtonEnabled = !hasErrorNotifications, notifications = notifications, + showTapHelp = sendState.showTapHelp && notifications.isEmpty(), ), ) } + + fun getHiddenTapHelpState(): SendUiState { + val state = currentStateProvider() + val sendState = state.sendState ?: return state + return state.copy( + sendState = sendState.copy(showTapHelp = false), + ) + } //endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index 00d7a9dbbe..5ac0949edf 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -15,7 +15,6 @@ import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal /** @@ -29,7 +28,7 @@ internal data class SendUiState( val amountState: SendStates.AmountState? = null, val recipientState: SendStates.RecipientState? = null, val feeState: SendStates.FeeState? = null, - val sendState: SendStates.SendState = SendStates.SendState(), + val sendState: SendStates.SendState? = null, val isBalanceHidden: Boolean, val event: StateEvent, ) @@ -84,14 +83,15 @@ internal sealed class SendStates { data class SendState( override val type: SendUiStateType = SendUiStateType.Send, override val isPrimaryButtonEnabled: Boolean = true, - val isSending: Boolean = false, - val isSuccess: Boolean = false, - val isSubtract: Boolean = false, - val transactionDate: Long = 0L, - val txUrl: String = "", - val ignoreAmountReduce: Boolean = false, - val isFromConfirmation: Boolean = true, - val notifications: ImmutableList = persistentListOf(), + val isSending: Boolean, + val isSuccess: Boolean, + val isSubtract: Boolean, + val transactionDate: Long, + val txUrl: String, + val ignoreAmountReduce: Boolean, + val isFromConfirmation: Boolean, + val showTapHelp: Boolean, + val notifications: ImmutableList, ) : SendStates() } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt index 4d00bd9b2e..bcad6f7468 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt @@ -31,7 +31,7 @@ internal class SendAmountSubtractConverter( val fiatValue = decimalFiatValue.parseBigDecimal(fiatDecimals) return state.copy( - sendState = state.sendState.copy(isSubtract = true), + sendState = state.sendState?.copy(isSubtract = true), amountState = amountState.copy( amountTextField = amountTextField.copy( value = cryptoValue, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendConfirmStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendConfirmStateConverter.kt new file mode 100644 index 0000000000..d5662a10b7 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendConfirmStateConverter.kt @@ -0,0 +1,25 @@ +package com.tangem.features.send.impl.presentation.state.confirm + +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf + +internal class SendConfirmStateConverter( + private val isTapHelpPreviewEnabledProvider: Provider, +) : Converter { + override fun convert(value: Unit): SendStates.SendState { + return SendStates.SendState( + isPrimaryButtonEnabled = true, + isSending = false, + isSuccess = false, + isSubtract = false, + transactionDate = 0L, + txUrl = "", + ignoreAmountReduce = false, + isFromConfirmation = true, + showTapHelp = isTapHelpPreviewEnabledProvider(), + notifications = persistentListOf(), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 0344343e70..59028beb85 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -32,7 +32,8 @@ import com.tangem.features.send.impl.presentation.state.SendUiStateType @Composable internal fun SendNavigationButtons(uiState: SendUiState, currentState: SendUiCurrentScreen) { - val isSuccess = uiState.sendState.isSuccess + val sendState = uiState.sendState ?: return + val isSuccess = sendState.isSuccess val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess val isSentState = currentState.type == SendUiStateType.Send && isSuccess Column( @@ -45,7 +46,7 @@ internal fun SendNavigationButtons(uiState: SendUiState, currentState: SendUiCur ) { SendingText(uiState = uiState, isVisible = isSendingState) SendDoneButtons( - txUrl = uiState.sendState.txUrl, + txUrl = sendState.txUrl, onExploreClick = uiState.clickIntents::onExploreClick, onShareClick = uiState.clickIntents::onShareClick, isVisible = isSentState, @@ -65,8 +66,9 @@ private fun SendNavigationButton( modifier: Modifier = Modifier, ) { val hapticFeedback = LocalHapticFeedback.current + val sendState = uiState.sendState ?: return val isEditingDisabled = uiState.isEditingDisabled - val isSuccess = uiState.sendState.isSuccess + val isSuccess = sendState.isSuccess val isFromConfirmation = currentState.isFromConfirmation val isCorrectScreen = currentState.type == SendUiStateType.Amount || currentState.type == SendUiStateType.Fee @@ -111,7 +113,7 @@ private fun SendNavigationButton( if (isSendingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) buttonClick() }, - showProgress = uiState.sendState.isSending, + showProgress = sendState.isSending, modifier = Modifier.fillMaxWidth(), colors = TangemButtonsDefaults.primaryButtonColors, ) @@ -226,7 +228,7 @@ private fun isButtonEnabled(currentState: SendUiCurrentScreen, uiState: SendUiSt SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false SendUiStateType.Fee -> uiState.feeState?.isPrimaryButtonEnabled ?: false - SendUiStateType.Send -> uiState.sendState.isPrimaryButtonEnabled + SendUiStateType.Send -> uiState.sendState?.isPrimaryButtonEnabled ?: false else -> true } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 729c3c9a24..91c44b07ab 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.StateFlow internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow) { val currentState = currentStateFlow.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } + val sendState = uiState.sendState ?: return BackHandler { uiState.clickIntents.onBackClick() } Column( modifier = Modifier @@ -44,7 +45,7 @@ internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow resourceReference(R.string.send_amount_label) SendUiStateType.Recipient -> resourceReference(R.string.send_recipient_label) SendUiStateType.Fee -> resourceReference(R.string.common_fee_selector_title) - SendUiStateType.Send -> if (!uiState.sendState.isSuccess) { + SendUiStateType.Send -> if (!sendState.isSuccess) { resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencySymbol)) } else { null diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 6cd08ba079..9dff71b1db 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.presentation.ui.send -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.* +import androidx.compose.animation.core.MutableTransitionState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -10,6 +11,8 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -22,14 +25,16 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendUiState import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.delay private const val TAP_HELP_KEY = "TAP_HELP_KEY" private const val BLOCKS_KEY = "BLOCKS_KEY" +private const val TAP_HELP_ANIMATION_DELAY = 500L @Suppress("LongMethod") @Composable internal fun SendContent(uiState: SendUiState) { - val sendState = uiState.sendState + val sendState = uiState.sendState ?: return LazyColumn( modifier = Modifier .fillMaxSize() @@ -37,7 +42,7 @@ internal fun SendContent(uiState: SendUiState) { verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { blocks(uiState) - tapHelp(isDisplay = sendState.notifications.isEmpty() && !uiState.sendState.isSuccess) + tapHelp(isDisplay = sendState.showTapHelp) notifications(sendState.notifications) } } @@ -46,7 +51,7 @@ private fun LazyListScope.blocks(uiState: SendUiState) { val amountState = uiState.amountState ?: return val recipientState = uiState.recipientState ?: return val feeState = uiState.feeState ?: return - val sendState = uiState.sendState + val sendState = uiState.sendState ?: return val isSuccess = sendState.isSuccess val timestamp = sendState.transactionDate @@ -84,8 +89,22 @@ private fun LazyListScope.blocks(uiState: SendUiState) { @OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) { - if (isDisplay) { - item(key = TAP_HELP_KEY) { + item(key = TAP_HELP_KEY) { + val animationState = remember { MutableTransitionState(false) } + + LaunchedEffect(key1 = isDisplay) { + delay(TAP_HELP_ANIMATION_DELAY) + animationState.targetState = isDisplay + } + + AnimatedVisibility( + visibleState = animationState, + label = "Tap Help Animation", + enter = slideInVertically( + initialOffsetY = { it / 2 }, + ).plus(fadeIn()), + exit = slideOutVertically().plus(fadeOut()), + ) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 073fb98d89..588ea6b0a2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -16,6 +16,8 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase +import com.tangem.domain.settings.NeverToShowTapHelpUseCase import com.tangem.domain.tokens.* import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency @@ -85,6 +87,8 @@ internal class SendViewModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val parseQrCodeUseCase: ParseQrCodeUseCase, + private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase, + private val neverToShowTapHelpUseCase: NeverToShowTapHelpUseCase, currencyChecksRepository: CurrencyChecksRepository, isFeeApproximateUseCase: IsFeeApproximateUseCase, getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, @@ -120,6 +124,7 @@ internal class SendViewModel @Inject constructor( feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, validateWalletMemoUseCase = validateWalletMemoUseCase, getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase, + isTapHelpPreviewEnabledProvider = Provider { isTapHelpPreviewEnabled }, ) private val amountStateFactory = AmountStateFactory( @@ -173,6 +178,7 @@ internal class SendViewModel @Inject constructor( private var userWallet: UserWallet by Delegates.notNull() private var isAmountSubtractAvailable: Boolean = false + private var isTapHelpPreviewEnabled: Boolean = false private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() @@ -192,6 +198,7 @@ internal class SendViewModel @Inject constructor( init { subscribeOnCurrencyStatusUpdates() subscribeOnBalanceHidden() + getTapHelpPreviewAvailability() } override fun onCreate(owner: LifecycleOwner) { @@ -286,6 +293,12 @@ internal class SendViewModel @Inject constructor( } } + private fun getTapHelpPreviewAvailability() { + viewModelScope.launch(dispatchers.main) { + isTapHelpPreviewEnabled = isSendTapHelpEnabledUseCase() + } + } + private fun getCoinCurrencyStatusUpdates(isSingleWalletWithToken: Boolean) = getNetworkCoinStatusUseCase( userWalletId = userWalletId, networkId = cryptoCurrency.network.id, @@ -344,7 +357,7 @@ internal class SendViewModel @Inject constructor( feeCryptoCurrencyStatus = feeCurrencyStatus when { - uiState.sendState.isSuccess -> { + uiState.sendState?.isSuccess == true -> { stateRouter.showSend() } transactionId != null && amount != null && destinationAddress != null -> { @@ -454,7 +467,7 @@ internal class SendViewModel @Inject constructor( // region screen state navigation override fun popBackStack() = stateRouter.popBackStack() - override fun onBackClick() = stateRouter.onBackClick(uiState.sendState.isSuccess) + override fun onBackClick() = stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true) override fun onNextClick() { val currentState = stateRouter.currentState.value val isCurrentFee = currentState.type == SendUiStateType.Fee @@ -656,7 +669,7 @@ internal class SendViewModel @Inject constructor( // region send state clicks override fun onSendClick() { - val sendState = uiState.sendState + val sendState = uiState.sendState ?: return if (sendState.isSuccess) popBackStack() uiState = stateFactory.getSendingStateUpdate(isSending = true) @@ -670,16 +683,20 @@ internal class SendViewModel @Inject constructor( override fun showAmount() { stateRouter.showAmount(isFromConfirmation = true) + setNeverToShowTapHelp() analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount)) } override fun showRecipient() { stateRouter.showRecipient(isFromConfirmation = true) + uiState = stateFactory.getHiddenTapHelpState() + setNeverToShowTapHelp() analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address)) } override fun showFee() { stateRouter.showFee(isFromConfirmation = true) + setNeverToShowTapHelp() analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) } @@ -688,8 +705,9 @@ internal class SendViewModel @Inject constructor( } override fun onExploreClick() { + val sendState = uiState.sendState ?: return analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked) - innerRouter.openUrl(uiState.sendState.txUrl) + innerRouter.openUrl(sendState.txUrl) } override fun onShareClick() { @@ -772,8 +790,9 @@ internal class SendViewModel @Inject constructor( } private fun onCheckFeeUpdate() { - val isSuccess = uiState.sendState.isSuccess - val noErrorNotifications = uiState.sendState.notifications.none { it is SendNotification.Error } + val sendState = uiState.sendState ?: return + val isSuccess = sendState.isSuccess + val noErrorNotifications = sendState.notifications.none { it is SendNotification.Error } if (!isSuccess && noErrorNotifications) { viewModelScope.launch(dispatchers.main) { @@ -809,6 +828,13 @@ internal class SendViewModel @Inject constructor( } } } + + private fun setNeverToShowTapHelp() { + viewModelScope.launch(dispatchers.main) { + neverToShowTapHelpUseCase() + } + uiState = stateFactory.getHiddenTapHelpState() + } // endregion companion object { From 328271bc26f6a7f629c12679d41de43509194259 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Mar 2024 15:31:40 +0100 Subject: [PATCH 036/109] Updated on 2026-08-14 --- app/src/main/assets/testnet_tokens.json | 40 ++++++++++++++ .../core/ui/extensions/BlockchainIcons.kt | 24 +++++++- core/ui/src/main/res/drawable/ic_manta_22.xml | 10 ++++ .../src/main/res/drawable/ic_moonbeam_22.xml | 48 ++++++++++++++++ .../src/main/res/drawable/ic_polygon_16.xml | 10 ---- .../src/main/res/drawable/ic_polygon_22.xml | 9 +++ .../ui/src/main/res/drawable/ic_zksync_22.xml | 14 +++++ .../ui/src/main/res/drawable/img_manta_22.xml | 30 ++++++++++ .../src/main/res/drawable/img_moonbeam_22.xml | 55 +++++++++++++++++++ .../src/main/res/drawable/img_polygon_22.xml | 26 +++++++-- .../src/main/res/drawable/img_zksync_22.xml | 21 +++++++ .../QuotesUnsupportedCurrenciesIdAdapter.kt | 3 + .../domain/common/extensions/Blockchain.kt | 35 ++++++++---- gradle/dependencies.toml | 2 +- 14 files changed, 297 insertions(+), 30 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_manta_22.xml create mode 100644 core/ui/src/main/res/drawable/ic_moonbeam_22.xml delete mode 100644 core/ui/src/main/res/drawable/ic_polygon_16.xml create mode 100644 core/ui/src/main/res/drawable/ic_polygon_22.xml create mode 100644 core/ui/src/main/res/drawable/ic_zksync_22.xml create mode 100644 core/ui/src/main/res/drawable/img_manta_22.xml create mode 100644 core/ui/src/main/res/drawable/img_moonbeam_22.xml create mode 100644 core/ui/src/main/res/drawable/img_zksync_22.xml diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index b638128b34..423202eeda 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -638,6 +638,46 @@ "networkId": "pulsechain/test" } ] + }, + { + "id": "zksync-ethereum", + "name": "zkSync", + "symbol": "ETH", + "networks": [ + { + "networkId": "zksync/test" + } + ] + }, + { + "id": "moonbeam", + "name": "Moonbeam", + "symbol": "GLMR", + "networks": [ + { + "networkId": "moonbeam/test" + } + ] + }, + { + "id": "manta-network-ethereum", + "name": "Manta Testnet", + "symbol": "ETH", + "networks": [ + { + "networkId": "manta-network/test" + } + ] + }, + { + "id": "polygon-zkevm-ethereum", + "name": "Polygon zkEvm Testnet", + "symbol": "ETH", + "networks": [ + { + "networkId": "polygon-zkevm/test" + } + ] } ] } diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 0854deb7f8..d7b3fd7f1d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -57,6 +57,10 @@ fun getActiveIconRes(blockchainId: String): Int { "aurora", "aurora/test" -> R.drawable.img_aurora_22 "areon", "areon/test" -> R.drawable.img_areon_22 "pls", "pls/test" -> R.drawable.img_pls_22 + "zkSyncEra", "zkSyncEra/test" -> R.drawable.img_zksync_22 + "moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22 + "manta", "manta/test" -> R.drawable.img_manta_22 + "polygonZkEVM", "polygonZkEVM/test" -> R.drawable.img_polygon_22 "radiant" -> R.drawable.img_radiant_22 else -> R.drawable.ic_alert_24 } @@ -116,6 +120,10 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "aurora", "aurora/test" -> R.drawable.img_aurora_22 "areon", "areon/test" -> R.drawable.img_areon_22 "pls", "pls/test" -> R.drawable.img_pls_22 + "zksync", "zksync/test" -> R.drawable.img_zksync_22 + "moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22 + "manta-network", "manta-network/test" -> R.drawable.img_manta_22 + "polygon-zkevm", "polygon-zkevm/test" -> R.drawable.img_polygon_22 "radiant" -> R.drawable.img_radiant_22 else -> R.drawable.ic_alert_24 } @@ -172,6 +180,10 @@ fun getActiveIconResByCoinId(coinId: String): Int { "aurora-near" -> R.drawable.img_aurora_22 "areon" -> R.drawable.img_areon_22 "pls" -> R.drawable.img_pls_22 + "zksync-ethereum" -> R.drawable.img_zksync_22 + "moonbeam" -> R.drawable.img_moonbeam_22 + "manta-network-ethereum" -> R.drawable.img_manta_22 + "polygon-zkevm-ethereum" -> R.drawable.img_polygon_22 "radiant" -> R.drawable.img_radiant_22 else -> R.drawable.ic_alert_24 } @@ -193,7 +205,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "XRP" -> R.drawable.ic_xrp_22 "XLM", "XLM/test" -> R.drawable.ic_stellar_16 "AVALANCHE", "AVALANCHE/test" -> R.drawable.ic_avalanche_22 - "POLYGON", "POLYGON/test" -> R.drawable.ic_polygon_16 + "POLYGON", "POLYGON/test" -> R.drawable.ic_polygon_22 "SOLANA", "SOLANA/test" -> R.drawable.ic_solana_16 "FTM", "FTM/test" -> R.drawable.ic_fantom_22 "BSC", "BSC/test", "BINANCE", "BINANCE/test" -> R.drawable.ic_bsc_16 @@ -231,6 +243,10 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "aurora", "aurora/test" -> R.drawable.ic_aurora_22 "areon", "areon/test" -> R.drawable.ic_areon_22 "pls", "pls/test" -> R.drawable.ic_pls_22 + "zkSyncEra", "zkSyncEra/test" -> R.drawable.ic_zksync_22 + "moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22 + "manta", "manta/test" -> R.drawable.ic_manta_22 + "polygonZkEVM", "polygonZkEVM/test" -> R.drawable.ic_polygon_22 "radiant" -> R.drawable.ic_radiant_22 else -> R.drawable.ic_alert_24 } @@ -253,7 +269,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "tezos" -> R.drawable.ic_tezos_16 "xrp", "ripple" -> R.drawable.ic_xrp_22 "stellar", "stellar/test" -> R.drawable.ic_stellar_16 - "polygon-pos", "polygon-pos/test" -> R.drawable.ic_polygon_16 + "polygon-pos", "polygon-pos/test" -> R.drawable.ic_polygon_22 "solana", "solana/test" -> R.drawable.ic_solana_16 "fantom", "fantom/test" -> R.drawable.ic_fantom_22 "dogecoin" -> R.drawable.ic_dogecoin_16 @@ -290,6 +306,10 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "aurora", "aurora/test" -> R.drawable.ic_aurora_22 "areon", "areon/test" -> R.drawable.ic_areon_22 "pls", "pls/test" -> R.drawable.ic_pls_22 + "zksync", "zksync/test" -> R.drawable.ic_zksync_22 + "moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22 + "manta-network", "manta-network/test" -> R.drawable.ic_manta_22 + "polygon-zkevm", "polygon-zkevm/test" -> R.drawable.ic_polygon_22 "radiant" -> R.drawable.ic_radiant_22 else -> R.drawable.ic_alert_24 } diff --git a/core/ui/src/main/res/drawable/ic_manta_22.xml b/core/ui/src/main/res/drawable/ic_manta_22.xml new file mode 100644 index 0000000000..0cdfc8df09 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_manta_22.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_moonbeam_22.xml b/core/ui/src/main/res/drawable/ic_moonbeam_22.xml new file mode 100644 index 0000000000..71e2200421 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_moonbeam_22.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_polygon_16.xml b/core/ui/src/main/res/drawable/ic_polygon_16.xml deleted file mode 100644 index ddc6ff8adf..0000000000 --- a/core/ui/src/main/res/drawable/ic_polygon_16.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/core/ui/src/main/res/drawable/ic_polygon_22.xml b/core/ui/src/main/res/drawable/ic_polygon_22.xml new file mode 100644 index 0000000000..73d1249583 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_polygon_22.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_zksync_22.xml b/core/ui/src/main/res/drawable/ic_zksync_22.xml new file mode 100644 index 0000000000..16964a64d6 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_zksync_22.xml @@ -0,0 +1,14 @@ + + + + diff --git a/core/ui/src/main/res/drawable/img_manta_22.xml b/core/ui/src/main/res/drawable/img_manta_22.xml new file mode 100644 index 0000000000..93200c11c3 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_manta_22.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_moonbeam_22.xml b/core/ui/src/main/res/drawable/img_moonbeam_22.xml new file mode 100644 index 0000000000..7c010ab7e4 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_moonbeam_22.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_polygon_22.xml b/core/ui/src/main/res/drawable/img_polygon_22.xml index efb2fff8c1..455e3cbe53 100644 --- a/core/ui/src/main/res/drawable/img_polygon_22.xml +++ b/core/ui/src/main/res/drawable/img_polygon_22.xml @@ -1,13 +1,29 @@ + + + android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z" + android:fillColor="#F5F0FD"/> + android:pathData="M7.435,8.666C7.694,8.514 8.03,8.514 8.314,8.666L10.328,9.833L11.697,10.594L13.712,11.761C13.97,11.913 14.306,11.913 14.59,11.761L16.192,10.848C16.45,10.696 16.631,10.417 16.631,10.112V8.311C16.631,8.006 16.476,7.727 16.192,7.575L14.616,6.687C14.358,6.535 14.022,6.535 13.738,6.687L12.162,7.575C11.904,7.727 11.723,8.006 11.723,8.311V9.478L10.354,8.691V7.524C10.354,7.22 10.509,6.941 10.793,6.789L13.712,5.114C13.97,4.962 14.306,4.962 14.59,5.114L17.561,6.789C17.845,6.941 18,7.22 18,7.524V10.899C18,11.203 17.845,11.482 17.561,11.634L14.59,13.309C14.332,13.461 13.996,13.461 13.712,13.309L11.697,12.167L10.328,11.38L8.314,10.239C8.055,10.087 7.72,10.087 7.435,10.239L5.86,11.127C5.601,11.279 5.421,11.558 5.421,11.863V13.664C5.421,13.968 5.576,14.247 5.86,14.4L7.435,15.313C7.694,15.465 8.03,15.465 8.314,15.313L9.889,14.425C10.148,14.273 10.328,13.994 10.328,13.689V12.522L11.697,13.309V14.476C11.697,14.78 11.542,15.059 11.258,15.211L8.288,16.886C8.03,17.038 7.694,17.038 7.41,16.886L4.439,15.211C4.181,15.059 4,14.78 4,14.476V11.101C4,10.797 4.155,10.518 4.439,10.366L7.435,8.666Z"> + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_zksync_22.xml b/core/ui/src/main/res/drawable/img_zksync_22.xml new file mode 100644 index 0000000000..ffb720dae9 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_zksync_22.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt index e6d573b353..6b2779299c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt @@ -51,6 +51,9 @@ internal class QuotesUnsupportedCurrenciesIdAdapter { private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = mapOf( "optimistic-ethereum" to "ethereum", "arbitrum-one" to "ethereum", + "zksync-ethereum" to "ethereum", + "manta-network-ethereum" to "ethereum", + "polygon-zkevm-ethereum" to "ethereum", "aurora-ethereum" to "ethereum", ) } diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index fdbe2e0fac..2f27ff5330 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -93,10 +93,16 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "areon-network/test" -> Blockchain.AreonTestnet "pulsechain" -> Blockchain.PulseChain "pulsechain/test" -> Blockchain.PulseChainTestnet + "zksync" -> Blockchain.ZkSyncEra + "zksync/test" -> Blockchain.ZkSyncEraTestnet + "moonbeam" -> Blockchain.Moonbeam + "moonbeam/test" -> Blockchain.MoonbeamTestnet + "manta-network" -> Blockchain.Manta + "manta-network/test" -> Blockchain.MantaTestnet + "polygon-zkevm" -> Blockchain.PolygonZkEVM + "polygon-zkevm/test" -> Blockchain.PolygonZkEVMTestnet "nexa" -> Blockchain.Nexa // FIXME - "nexa/testnet" -> Blockchain.NexaTestnet // FIXME - "zksync" -> Blockchain.ZkSyncEra // FIXME - "zksync/testnet" -> Blockchain.ZkSyncEraTestnet // FIXME + "nexa/test" -> Blockchain.NexaTestnet // FIXME "radiant" -> Blockchain.Radiant else -> null } @@ -192,10 +198,16 @@ fun Blockchain.toNetworkId(): String { Blockchain.AreonTestnet -> "areon-network/test" Blockchain.PulseChain -> "pulsechain" Blockchain.PulseChainTestnet -> "pulsechain/test" - Blockchain.ZkSyncEra -> "zksync" // FIXME - Blockchain.ZkSyncEraTestnet -> "zksync/testnet" // FIXME + Blockchain.ZkSyncEra -> "zksync" + Blockchain.ZkSyncEraTestnet -> "zksync/test" + Blockchain.Moonbeam -> "moonbeam" + Blockchain.MoonbeamTestnet -> "moonbeam/test" + Blockchain.Manta -> "manta-network" + Blockchain.MantaTestnet -> "manta-network/test" + Blockchain.PolygonZkEVM -> "polygon-zkevm" + Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm/test" Blockchain.Nexa -> "nexa" // FIXME - Blockchain.NexaTestnet -> "nexa/testnet" // FIXME + Blockchain.NexaTestnet -> "nexa/test" // FIXME Blockchain.Radiant -> "radiant" } } @@ -259,10 +271,11 @@ fun Blockchain.toCoinId(): String { Blockchain.Aurora, Blockchain.AuroraTestnet -> "aurora-ethereum" Blockchain.Areon, Blockchain.AreonTestnet -> "areon-network" Blockchain.PulseChain, Blockchain.PulseChainTestnet -> "pulsechain" - Blockchain.ZkSyncEra -> "zksync" // FIXME - Blockchain.ZkSyncEraTestnet -> "zksync/testnet" // FIXME - Blockchain.Nexa -> "nexa" // FIXME - Blockchain.NexaTestnet -> "nexa/testnet" // FIXME + Blockchain.ZkSyncEra, Blockchain.ZkSyncEraTestnet -> "zksync-ethereum" + Blockchain.Moonbeam, Blockchain.MoonbeamTestnet -> "moonbeam" + Blockchain.Manta, Blockchain.MantaTestnet -> "manta-network-ethereum" + Blockchain.PolygonZkEVM, Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm-ethereum" + Blockchain.Nexa, Blockchain.NexaTestnet -> "nexa" // FIXME Blockchain.Radiant -> "radiant" } } @@ -291,8 +304,6 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Playa3ull, - Blockchain.ZkSyncEra, - Blockchain.ZkSyncEraTestnet, Blockchain.Nexa, Blockchain.NexaTestnet, Blockchain.Radiant, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index df3dce1313..059989055e 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,7 +85,7 @@ leakcanary = "2.13" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-538" +tangemBlockchainSdk = "develop-539" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-337" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 8b6944e3e4c2767cbe90f1d62f8fb5d614e67e59 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Mar 2024 12:02:58 +0300 Subject: [PATCH 037/109] Updated on 2026-08-14 --- .../com/tangem/core/ui/extensions/String.kt | 4 +- .../core/ui/utils/DateTimeFormatters.kt | 6 +- .../com/tangem/core/ui/utils/DateUtils.kt | 2 +- data/visa/build.gradle.kts | 2 +- .../tangem/data/visa/DefaultVisaRepository.kt | 6 +- .../data/visa/di/ImplementedVisaDataModule.kt | 53 ++ .../tangem/data/visa/utils/CurrencyUtils.kt | 0 .../com/tangem/data/visa/utils/VisaConfig.kt | 0 .../data/visa/utils/VisaCurrencyFactory.kt | 5 +- .../data/visa/utils/VisaTxDetailsFactory.kt | 0 .../visa/utils/VisaTxHistoryItemFactory.kt | 0 .../visa/utils/VisaTxHistoryPagingSource.kt | 0 .../tangem/data/visa/DummyVisaRepository.kt | 28 + .../data/visa/di/ImplementedVisaRepository.kt | 6 + .../di/ImplementedVisaRepositoryModule.kt | 16 + .../com/tangem/data/visa/di/VisaDataModule.kt | 38 +- .../BalancesAndLimitsBottomSheetConfig.kt | 2 +- .../wallet/state/model/WalletAlertState.kt | 29 +- .../BalancesAndLimitsBottomSheetConverter.kt | 13 +- .../VisaTxDetailsBottomSheetConverter.kt | 17 +- .../VisaTxHistoryItemStateConverter.kt | 7 +- .../subscribers/VisaWalletSubscriber.kt | 1 + .../components/visa/BalancesAndLimitsBlock.kt | 8 +- .../visa/BalancesAndLimitsBottomSheet.kt | 6 +- .../main/java/com/tangem/lib/visa/ERC20.java | 45 +- .../lib/visa/TangemBridgeProcessor.java | 739 +++++++++------- .../tangem/lib/visa/TangemPaymentAccount.java | 832 ++++++++++-------- .../visa/TangemPaymentAccountRegistry.java | 362 ++++++++ .../visa/DefaultVisaContractInfoProvider.kt | 48 +- .../lib/visa/VisaContractInfoProvider.kt | 2 + .../com/tangem/lib/visa/utils/VisaConfig.kt | 5 +- 31 files changed, 1464 insertions(+), 818 deletions(-) rename data/visa/src/{main => debug}/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt (95%) create mode 100644 data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt rename data/visa/src/{main => debug}/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt (100%) rename data/visa/src/{main => debug}/kotlin/com/tangem/data/visa/utils/VisaConfig.kt (100%) rename data/visa/src/{main => debug}/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt (92%) rename data/visa/src/{main => debug}/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt (100%) rename data/visa/src/{main => debug}/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt (100%) rename data/visa/src/{main => debug}/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt (100%) create mode 100644 data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepository.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepositoryModule.kt create mode 100644 libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccountRegistry.java diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt index f5cc3cb971..be264409bc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt @@ -29,4 +29,6 @@ fun String.toQrCode(sizePx: Int = 256, paddingPx: Int = 0): Bitmap { } } return bmp -} \ No newline at end of file +} + +fun String.capitalize(): String = replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index ce8b600e55..0ea40469e2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -59,11 +59,7 @@ object DateTimeFormatters { DateTimeFormat.forPattern("dd.MM.yyyy HH:mm") } - fun formatTime(formatter: DateTimeFormatter = timeFormatter, time: DateTime): String { - return formatter.print(time) - } - - fun formatDate(formatter: DateTimeFormatter = dateFormatter, date: DateTime): String { + fun formatDate(date: DateTime, formatter: DateTimeFormatter = dateFormatter): String { return formatter.print(date) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt index b0237e897a..8d596fc41f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt @@ -29,5 +29,5 @@ fun Long.toDateFormat(formatter: DateTimeFormatter = DateTimeFormatters.dateForm * Returns formatted time according to [formatter]. */ fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeFormatter): String { - return DateTimeFormatters.formatTime(formatter = formatter, time = DateTime(this, DateTimeZone.getDefault())) + return DateTimeFormatters.formatDate(date = DateTime(this, DateTimeZone.getDefault()), formatter = formatter) } \ No newline at end of file diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index d41cf5f9c7..ac55319dfe 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -26,7 +26,7 @@ dependencies { implementation(projects.domain.legacy) /** Project - Libs */ - implementation(projects.libs.visa) + debugImplementation(projects.libs.visa) /** Libs - Other */ implementation(deps.kotlin.coroutines) diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt similarity index 95% rename from data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index d14a201a2b..89752a8b76 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -58,7 +58,7 @@ internal class DefaultVisaRepository( override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency { val address = makeAddress(userWalletId) - // val address = "0x143fe062a538176aa0bf162f13d390208f90898f" // for testing + // val address = "0x40d8194b7168723ece51fa34d16825c60ba03dfa" // for testing fetchVisaCurrencyIfExpired(address, isRefresh) @@ -97,7 +97,7 @@ internal class DefaultVisaRepository( ): Flow> { val userWallet = findVisaUserWallet(userWalletId) val cardPubKey = getCardPubKey(userWallet).toHexString() - // val cardPubKey = "03DEF02B1FECC8BD3CFD52CE93235194479E1DE931EF0F55DC194967E7CCC3D12C" // for testing + // val cardPubKey = "02C2BBA0DA1E066EA968C1EB129499F6DEBC5FD82D70D61DCAF691CDB69AF5D8B9" // for testing val pager = Pager( config = PagingConfig( pageSize = pageSize, @@ -125,7 +125,7 @@ internal class DefaultVisaRepository( return withContext(dispatchers.io) { val userWallet = findVisaUserWallet(userWalletId) val cardPubKey = getCardPubKey(userWallet).toHexString() - // val cardPubKey = "03DEF02B1FECC8BD3CFD52CE93235194479E1DE931EF0F55DC194967E7CCC3D12C" // for testing + // val cardPubKey = "02C2BBA0DA1E066EA968C1EB129499F6DEBC5FD82D70D61DCAF691CDB69AF5D8B9" // for testing val transaction = fetchedHistoryItems.value[cardPubKey]?.firstOrNull { it.transactionId.toString() == txId } diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt new file mode 100644 index 0000000000..a3022a444a --- /dev/null +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt @@ -0,0 +1,53 @@ +package com.tangem.data.visa.di + +import com.squareup.moshi.Moshi +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.visa.BuildConfig +import com.tangem.data.visa.DefaultVisaRepository +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.visa.repository.VisaRepository +import com.tangem.lib.visa.VisaContractInfoProvider +import com.tangem.lib.visa.api.VisaApiBuilder +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object ImplementedVisaDataModule { + + @Provides + @Singleton + @ImplementedVisaRepository + fun provideVisaRepository( + @NetworkMoshi moshi: Moshi, + tangemTechApi: TangemTechApi, + cacheRegistry: CacheRegistry, + userWalletsStore: UserWalletsStore, + dispatchers: CoroutineDispatcherProvider, + ): VisaRepository { + val contractInfoProvider = VisaContractInfoProvider.Builder( + isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, + dispatchers = dispatchers, + ).build() + val visaApi = VisaApiBuilder( + useDevApi = true, + isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, + moshi = moshi, + ).build() + + return DefaultVisaRepository( + contractInfoProvider, + tangemTechApi, + visaApi, + cacheRegistry, + userWalletsStore, + dispatchers, + ) + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt similarity index 100% rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaConfig.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt similarity index 100% rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaConfig.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt similarity index 92% rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt index 92bb35d307..8f3e95d73a 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt @@ -17,6 +17,7 @@ internal class VisaCurrencyFactory { } else { balancesAndLimits.newLimits } + val remainingOtpLimit = getRemainingOtp(currentLimit, now) return VisaCurrency( symbol = VisaConfig.TOKEN_SYMBOL, @@ -35,8 +36,8 @@ internal class VisaCurrencyFactory { ) }, limits = VisaCurrency.Limits( - remainingOtp = getRemainingOtp(currentLimit, now), - remainingNoOtp = getRemainingNoOtp(currentLimit, now), + remainingOtp = remainingOtpLimit, + remainingNoOtp = minOf(remainingOtpLimit, getRemainingNoOtp(currentLimit, now)), singleTransaction = currentLimit.singleTransactionLimit, expirationDate = getLimitsExpirationDate(currentLimit, now), ), diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt similarity index 100% rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt similarity index 100% rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt similarity index 100% rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt new file mode 100644 index 0000000000..e965d71e93 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt @@ -0,0 +1,28 @@ +package com.tangem.data.visa + +import androidx.paging.PagingData +import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.visa.model.VisaTxDetails +import com.tangem.domain.visa.model.VisaTxHistoryItem +import com.tangem.domain.visa.repository.VisaRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +internal class DummyVisaRepository : VisaRepository { + + override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency { + TODO("Not implemented for this build type") + } + + override suspend fun getTxHistory( + userWalletId: UserWalletId, + pageSize: Int, + isRefresh: Boolean, + ): Flow> { + TODO("Not implemented for this build type") + } + + override suspend fun getTxDetails(userWalletId: UserWalletId, txId: String): VisaTxDetails { + TODO("Not implemented for this build type") + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepository.kt new file mode 100644 index 0000000000..074af6e72d --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepository.kt @@ -0,0 +1,6 @@ +package com.tangem.data.visa.di + +import javax.inject.Qualifier + +@Qualifier +internal annotation class ImplementedVisaRepository \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepositoryModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepositoryModule.kt new file mode 100644 index 0000000000..96de38914e --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepositoryModule.kt @@ -0,0 +1,16 @@ +package com.tangem.data.visa.di + +import com.tangem.domain.visa.repository.VisaRepository +import dagger.BindsOptionalOf +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface ImplementedVisaRepositoryModule { + + @BindsOptionalOf + @ImplementedVisaRepository + fun bindImplementedVisaRepository(): VisaRepository +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt index a0227da794..a8587868f9 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -1,21 +1,14 @@ package com.tangem.data.visa.di -import com.squareup.moshi.Moshi -import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.visa.BuildConfig -import com.tangem.data.visa.DefaultVisaRepository -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.di.NetworkMoshi -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.data.visa.DummyVisaRepository import com.tangem.domain.visa.repository.VisaRepository -import com.tangem.lib.visa.VisaContractInfoProvider -import com.tangem.lib.visa.api.VisaApiBuilder -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import java.util.Optional import javax.inject.Singleton +import kotlin.jvm.optionals.getOrNull @Module @InstallIn(SingletonComponent::class) @@ -24,29 +17,8 @@ internal object VisaDataModule { @Provides @Singleton fun provideVisaRepository( - @NetworkMoshi moshi: Moshi, - tangemTechApi: TangemTechApi, - cacheRegistry: CacheRegistry, - userWalletsStore: UserWalletsStore, - dispatchers: CoroutineDispatcherProvider, + @ImplementedVisaRepository implementedVisaRepository: Optional, ): VisaRepository { - val contractInfoProvider = VisaContractInfoProvider.Builder( - isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, - dispatchers = dispatchers, - ).build() - val visaApi = VisaApiBuilder( - useDevApi = true, - isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, - moshi = moshi, - ).build() - - return DefaultVisaRepository( - contractInfoProvider, - tangemTechApi, - visaApi, - cacheRegistry, - userWalletsStore, - dispatchers, - ) + return implementedVisaRepository.getOrNull() ?: DummyVisaRepository() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/BalancesAndLimitsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/BalancesAndLimitsBottomSheetConfig.kt index bfabae7770..1dd961ab14 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/BalancesAndLimitsBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/BalancesAndLimitsBottomSheetConfig.kt @@ -19,7 +19,7 @@ internal data class BalancesAndLimitsBottomSheetConfig( data class Limit( val availableBy: String, - val inStore: String, + val total: String, val other: String, val singleTransaction: String, val onInfoClick: () -> Unit, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt index 2b1df72669..b8aecf2d9f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt @@ -48,19 +48,32 @@ internal sealed interface WalletAlertState { override val isWarningConfirmButton: Boolean = true } - object WrongCardIsScanned : Basic() { + data class VisaLimitsInfo( + val totalLimit: String, + val otherLimit: String, + ) : Basic() { + override val title: TextReference? = null + override val message: TextReference = stringReference( + value = "Limits are needed to control costs, improve security, manage risk. " + + "You can spend $totalLimit during the week for card payments in shops and " + + "$otherLimit for other transactions, e. g. subscriptions or debts.", + ) + override val onConfirmClick: (() -> Unit)? = null + } + + data object WrongCardIsScanned : Basic() { override val title: TextReference = resourceReference(R.string.common_warning) override val message: TextReference = resourceReference(R.string.error_wrong_wallet_tapped) override val onConfirmClick: (() -> Unit)? = null } - object RescanWallets : Basic() { + data object RescanWallets : Basic() { override val title: TextReference = resourceReference(R.string.common_attention) override val message: TextReference = resourceReference(R.string.key_invalidated_warning_description) override val onConfirmClick: (() -> Unit)? = null } - object VisaBalancesInfo : Basic() { + data object VisaBalancesInfo : Basic() { override val title: TextReference? = null override val message: TextReference = stringReference( value = "Available balance is actual funds available, considering pending transactions, " + @@ -68,14 +81,4 @@ internal sealed interface WalletAlertState { ) override val onConfirmClick: (() -> Unit)? = null } - - object VisaLimitsInfo : Basic() { - override val title: TextReference? = null - override val message: TextReference = stringReference( - value = "Limits are needed to control costs, improve security, manage risk. " + - "You can spend 1 000 USDT during the week for card payments in shops and " + - "100 USDT for other transactions, e. g. subscriptions or debts.", - ) - override val onConfirmClick: (() -> Unit)? = null - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt index f43ec73629..146e196a65 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt @@ -21,6 +21,9 @@ internal class BalancesAndLimitsBottomSheetConverter( decimals = value.decimals, ) + val otpLimit = value.limits.remainingOtp.let(::formatAmount) + val noOtpLimit = value.limits.remainingNoOtp.let(::formatAmount) + return BalancesAndLimitsBottomSheetConfig( balance = BalancesAndLimitsBottomSheetConfig.Balance( totalBalance = value.balances.total.let(::formatAmount), @@ -33,10 +36,10 @@ internal class BalancesAndLimitsBottomSheetConverter( ), limit = BalancesAndLimitsBottomSheetConfig.Limit( availableBy = DateTimeFormatters.formatDate(date = value.limits.expirationDate), - inStore = value.limits.remainingOtp.let(::formatAmount), - other = value.limits.remainingNoOtp.let(::formatAmount), + total = otpLimit, + other = noOtpLimit, singleTransaction = value.limits.singleTransaction.let(::formatAmount), - onInfoClick = this::showLimitInfo, + onInfoClick = { showLimitInfo(otpLimit, noOtpLimit) }, ), ) } @@ -45,7 +48,7 @@ internal class BalancesAndLimitsBottomSheetConverter( eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaBalancesInfo)) } - private fun showLimitInfo() { - eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo)) + private fun showLimitInfo(totalLimit: String, otherLimit: String) { + eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo(totalLimit, otherLimit))) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt index f431a74a5e..6e8158da13 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter +import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency @@ -27,14 +28,14 @@ internal class VisaTxDetailsBottomSheetConverter( private fun createTransaction(details: VisaTxDetails): VisaTxDetailsBottomSheetConfig.Transaction { return VisaTxDetailsBottomSheetConfig.Transaction( id = details.id, - type = details.type, - status = details.status, + type = details.type.capitalize(), + status = details.status.capitalize(), blockchainAmount = formatNetworkAmount(details.blockchainAmount), blockchainFee = formatNetworkAmount(details.blockchainFee), transactionAmount = formatFiatAmount(details.transactionAmount, details.fiatCurrency), transactionCurrencyCode = details.transactionCurrencyCode.toString(), - merchantName = details.merchantName ?: UNKNOWN, - merchantCity = details.merchantCity ?: UNKNOWN, + merchantName = details.merchantName?.capitalize() ?: UNKNOWN, + merchantCity = details.merchantCity?.capitalize() ?: UNKNOWN, merchantCountryCode = details.merchantCountryCode ?: UNKNOWN, merchantCategoryCode = details.merchantCategoryCode ?: UNKNOWN, ) @@ -46,16 +47,16 @@ internal class VisaTxDetailsBottomSheetConverter( return VisaTxDetailsBottomSheetConfig.Request( id = request.id, - type = request.requestType, - status = request.requestStatus, + type = request.requestType.capitalize(), + status = request.requestStatus.capitalize(), blockchainAmount = formatNetworkAmount(request.blockchainAmount), blockchainFee = formatNetworkAmount(request.blockchainFee), transactionAmount = formatFiatAmount(request.transactionAmount, request.fiatCurrency), currencyCode = request.billingCurrencyCode.toString(), errorCode = request.errorCode, - date = DateTimeFormatters.formatDate(DateTimeFormatters.dateTimeFormatter, date = localDate), + date = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.dateTimeFormatter), txHash = request.txHash ?: UNKNOWN, - txStatus = request.txStatus ?: UNKNOWN, + txStatus = request.txStatus?.capitalize() ?: UNKNOWN, onExploreClick = if (exploreUrl != null) { { clickIntents.onExploreClick(exploreUrl) } } else { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt index bddc7ec7aa..5c1b0a20c5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters @@ -18,8 +19,8 @@ internal class VisaTxHistoryItemStateConverter( override fun convert(value: VisaTxHistoryItem): TransactionState { val localDate = value.date.withZone(DateTimeZone.getDefault()) - val time = DateTimeFormatters.formatTime(time = localDate) - val subtitle = "$time • ${value.status}" + val time = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.timeFormatter) + val subtitle = "$time • ${value.status.capitalize()}" return TransactionState.Content( txHash = value.id, @@ -37,7 +38,7 @@ internal class VisaTxHistoryItemStateConverter( status = TransactionState.Content.Status.Confirmed, direction = TransactionState.Content.Direction.INCOMING, iconRes = R.drawable.ic_arrow_up_24, - title = stringReference(value = value.merchantName ?: "Unknown merchant"), + title = stringReference(value = value.merchantName?.capitalize() ?: "Unknown merchant"), subtitle = stringReference(subtitle), timestamp = localDate.millis, onClick = { clickIntents.onVisaTransactionClick(value.id) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt index 552dbba1a5..9f6c2b8eb2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt @@ -42,6 +42,7 @@ internal class VisaWalletSubscriber( setLoadedCurrencyState(maybeCurrency) val currency = maybeCurrency.getOrElse { + Timber.e(it, "Failed to load VISA currency") setFailedTxHistoryState(it) return@flow } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt index 6fe4af13e5..332d7e3209 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt @@ -4,7 +4,10 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -60,7 +63,6 @@ private fun BalancesAndLimitsBlock(state: BalancesAndLimitsBlockState, modifier: ) } -@OptIn(ExperimentalMaterial3Api::class) @Composable private inline fun ContentContainer( enabled: Boolean, @@ -147,7 +149,7 @@ private fun AvailableLimit(availableBalance: String, limitDays: Int, modifier: M color = TangemTheme.colors.text.primary1, ) Text( - text = "available $limitDays-day limit", + text = "available for $limitDays day(s)", style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt index c2e6a39b4d..b4a5b96ffc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt @@ -94,8 +94,8 @@ private fun LimitsBlock(limits: BalancesAndLimitsBottomSheetConfig.Limit, modifi title = stringReference("Limits"), content = { BlockItem( - title = stringReference("In-store (otp)"), - value = limits.inStore, + title = stringReference("Total"), + value = limits.total, ) BlockItem( title = stringReference("Other (no-otp)"), @@ -193,7 +193,7 @@ private class BalancesAndLimitsBottomSheetParameterProvider : ), limit = BalancesAndLimitsBottomSheetConfig.Limit( availableBy = "Nov, 11 USDT", - inStore = "563.00 USDT", + total = "563.00 USDT", other = "100.00 USDT", singleTransaction = "100.00 USDT", onInfoClick = {}, diff --git a/libs/visa/src/main/java/com/tangem/lib/visa/ERC20.java b/libs/visa/src/main/java/com/tangem/lib/visa/ERC20.java index 7b92736361..fb6048f082 100644 --- a/libs/visa/src/main/java/com/tangem/lib/visa/ERC20.java +++ b/libs/visa/src/main/java/com/tangem/lib/visa/ERC20.java @@ -2,10 +2,7 @@ package com.tangem.lib.visa; import org.web3j.abi.EventEncoder; import org.web3j.abi.TypeReference; -import org.web3j.abi.datatypes.Address; -import org.web3j.abi.datatypes.Event; -import org.web3j.abi.datatypes.Function; -import org.web3j.abi.datatypes.Utf8String; +import org.web3j.abi.datatypes.*; import org.web3j.abi.datatypes.generated.Uint256; import org.web3j.abi.datatypes.generated.Uint8; import org.web3j.crypto.Credentials; @@ -32,10 +29,10 @@ import io.reactivex.Flowable; *

Auto generated code. *

Do not modify! *

Please use the web3j command line tools, - * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the + * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the * codegen module to update. * - *

Generated with web3j version 1.5.0. + *

Generated with web3j version 1.5.2. */ @SuppressWarnings("rawtypes") class ERC20 extends Contract { @@ -113,6 +110,16 @@ class ERC20 extends Contract { return typedResponse; } + @Deprecated + public static ERC20 load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + return new ERC20(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + @Deprecated + public static ERC20 load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + return new ERC20(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + public static List getTransferEvents(TransactionReceipt transactionReceipt) { List valueList = staticExtractEventParametersWithLog(TRANSFER_EVENT, transactionReceipt); ArrayList responses = new ArrayList(valueList.size()); @@ -127,16 +134,6 @@ class ERC20 extends Contract { return responses; } - @Deprecated - public static ERC20 load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { - return new ERC20(contractAddress, web3j, credentials, gasPrice, gasLimit); - } - - @Deprecated - public static ERC20 load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { - return new ERC20(contractAddress, web3j, transactionManager, gasPrice, gasLimit); - } - public static TransferEventResponse getTransferEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(TRANSFER_EVENT, log); TransferEventResponse typedResponse = new TransferEventResponse(); @@ -147,14 +144,6 @@ class ERC20 extends Contract { return typedResponse; } - public static ERC20 load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { - return new ERC20(contractAddress, web3j, credentials, contractGasProvider); - } - - public static ERC20 load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { - return new ERC20(contractAddress, web3j, transactionManager, contractGasProvider); - } - public Flowable transferEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getTransferEventFromLog(log)); } @@ -165,6 +154,14 @@ class ERC20 extends Contract { return transferEventFlowable(filter); } + public static ERC20 load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + return new ERC20(contractAddress, web3j, credentials, contractGasProvider); + } + + public static ERC20 load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return new ERC20(contractAddress, web3j, transactionManager, contractGasProvider); + } + public Flowable approvalEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getApprovalEventFromLog(log)); } diff --git a/libs/visa/src/main/java/com/tangem/lib/visa/TangemBridgeProcessor.java b/libs/visa/src/main/java/com/tangem/lib/visa/TangemBridgeProcessor.java index f712f5cb24..b612de84bc 100644 --- a/libs/visa/src/main/java/com/tangem/lib/visa/TangemBridgeProcessor.java +++ b/libs/visa/src/main/java/com/tangem/lib/visa/TangemBridgeProcessor.java @@ -3,10 +3,7 @@ package com.tangem.lib.visa; import org.web3j.abi.EventEncoder; import org.web3j.abi.FunctionEncoder; import org.web3j.abi.TypeReference; -import org.web3j.abi.datatypes.Address; -import org.web3j.abi.datatypes.Bool; -import org.web3j.abi.datatypes.Event; -import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.*; import org.web3j.abi.datatypes.generated.Bytes32; import org.web3j.abi.datatypes.generated.Bytes4; import org.web3j.abi.datatypes.generated.Uint256; @@ -35,14 +32,14 @@ import io.reactivex.Flowable; *

Auto generated code. *

Do not modify! *

Please use the web3j command line tools, - * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the + * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the * codegen module to update. * - *

Generated with web3j version 1.5.0. + *

Generated with web3j version 1.5.2. */ @SuppressWarnings("rawtypes") class TangemBridgeProcessor extends Contract { - public static final String BINARY = "608060405262278d0060085534801562000017575f80fd5b5060405162002457380380620024578339810160408190526200003a91620001fa565b600580546001600160a01b038088166001600160a01b031992831617909255600680548784169083161790556002805486841690831617905560038054928516929091169190911790556009819055620000955f33620000a1565b5050505050506200025d565b5f80620000af8484620000dc565b90508015620000d3575f848152600160205260409020620000d1908462000187565b505b90505b92915050565b5f828152602081815260408083206001600160a01b038516845290915281205460ff166200017f575f838152602081815260408083206001600160a01b03861684529091529020805460ff19166001179055620001363390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a4506001620000d6565b505f620000d6565b5f620000d3836001600160a01b0384165f8181526001830160205260408120546200017f57508154600181810184555f848152602080822090930184905584548482528286019093526040902091909155620000d6565b80516001600160a01b0381168114620001f5575f80fd5b919050565b5f805f805f60a086880312156200020f575f80fd5b6200021a86620001de565b94506200022a60208701620001de565b93506200023a60408701620001de565b92506200024a60608701620001de565b9150608086015190509295509295909350565b6121ec806200026b5f395ff3fe608060405234801561000f575f80fd5b506004361061024a575f3560e01c806391792d5b11610140578063c904ea39116100bf578063d6d32b2a11610084578063d6d32b2a14610551578063da90f3a014610564578063e24fd1f114610577578063e452f20d1461058a578063e96ede8e1461059d578063fe2208aa146105b0575f80fd5b8063c904ea39146104fc578063ca15c8731461050f578063cb37f3b214610522578063d47ae89c14610535578063d547741f1461053e575f80fd5b8063a217fddf11610105578063a217fddf14610495578063b6878ac91461049c578063b77c6d35146104af578063be4d1851146104c2578063c6b273e4146104d5575f80fd5b806391792d5b1461041757806391d14854146104205780639353ce4c1461043357806396bc563d1461045a578063978975591461046e575f80fd5b80634413e098116101cc57806373d3467c1161019157806373d3467c146103b85780637f2fadb0146103cb5780638072a022146103de57806383af133d146103f15780639010d07c14610404575f80fd5b80634413e098146103445780635673795114610357578063601c60651461036b57806365ebf99a1461037e57806370e4306e14610391575f80fd5b80632f2ff15d116102125780632f2ff15d146102cc5780633013ce29146102df57806336568abe1461030a57806337de81061461031d5780633d409a8614610330575f80fd5b806301ffc9a71461024e5780630f1071be1461027657806311dce7711461028d578063248a9ca3146102a25780632cc32641146102c4575b5f80fd5b61026161025c366004611c84565b6105c3565b60405190151581526020015b60405180910390f35b61027f60085481565b60405190815260200161026d565b6102a061029b366004611cbf565b6105ed565b005b61027f6102b0366004611cda565b5f9081526020819052604090206001015490565b6102a061065a565b6102a06102da366004611cf1565b6106d2565b6003546102f2906001600160a01b031681565b6040516001600160a01b03909116815260200161026d565b6102a0610318366004611cf1565b6106fc565b6102a061032b366004611cda565b610734565b61027f5f8051602061216e83398151915281565b6102a0610352366004611d1f565b610780565b61027f5f805160206120f983398151915281565b6102a0610379366004611d49565b610847565b6102a061038c366004611cbf565b610a6a565b61027f7f6ac2c8b5322a929d300af17766189693ff6db6939ffe690430ad4720baf47d1c81565b6102a06103c6366004611d88565b610acf565b6102a06103d9366004611d1f565b610bdc565b6102a06103ec366004611cda565b610cb1565b6102a06103ff366004611d1f565b610d3b565b6102f2610412366004611e06565b610df5565b61027f60075481565b61026161042e366004611cf1565b610e13565b61027f7f79af5a85184b45263c1b3721b9c8eb1becd98e55ade5a61da0fed18b90b8325681565b61027f5f8051602061204983398151915281565b61027f7f581be84b4822abe392a3bcac0d4d656e4cd57faf2e321a32108215cdd4a8365681565b61027f5f81565b6102a06104aa366004611e26565b610e3b565b6002546102f2906001600160a01b031681565b6006546102f2906001600160a01b031681565b61027f7f56d36dba8e2d5c88fc1038bd95811d491e67c9a66c6848c9d4c9309b9587e06481565b6102a061050a366004611e5e565b610f3f565b61027f61051d366004611cda565b611030565b6005546102f2906001600160a01b031681565b61027f60095481565b6102a061054c366004611cf1565b611046565b6102a061055f366004611cda565b61106a565b6102a0610572366004611d1f565b6110f3565b6102a0610585366004611ea5565b611357565b6102f2610598366004611cbf565b611495565b6102a06105ab366004611d49565b6114f3565b6102a06105be366004611d49565b6115d6565b5f6001600160e01b03198216635a05180f60e01b14806105e757506105e7826116a1565b92915050565b5f80516020612049833981519152610604816116d5565b600680546001600160a01b0319166001600160a01b0384169081179091556040519081527f68c7b435e0ad7bfb2140fab0735300efbda206f62906049e0b35763cef67cc5e906020015b60405180910390a15050565b600a543390610668826116e2565b5f811160405180606001604052806029815260200161218e60299139906106ab5760405162461bcd60e51b81526004016106a29190611ef3565b60405180910390fd5b505f600a556006546003546106ce916001600160a01b0391821691168484611785565b5050565b5f828152602081905260409020600101546106ec816116d5565b6106f683836117df565b50505050565b6001600160a01b03811633146107255760405163334bd91960e11b815260040160405180910390fd5b61072f8282611812565b505050565b5f8051602061204983398151915261074b816116d5565b60078290556040518281527fb5aa183eb20407e22587bcd13d5c82a85835a4bd60e2a13d7c7efee3c2e9ed489060200161064e565b7f581be84b4822abe392a3bcac0d4d656e4cd57faf2e321a32108215cdd4a836566107aa816116d5565b60405163b9603bdf60e01b8152600481018390526001600160a01b0384169063b9603bdf906024015f604051808303815f87803b1580156107e9575f80fd5b505af11580156107fb573d5f803e3d5ffd5b50505050826001600160a01b03167fc8818db5b3e4e986e2f21e002090b1513e17c05a7a326c3737af624bf6ef78c48360405161083a91815260200190565b60405180910390a2505050565b5f805160206120f983398151915261085e816116d5565b60408051808201909152601b81527f353130307c436f6d6d6f6e3a20616d6f756e74206973207a65726f00000000006020820152826108b05760405162461bcd60e51b81526004016106a29190611ef3565b505f6108bb85611495565b6003546040516370a0823160e01b81526001600160a01b0380841660048301529293505f92909116906370a0823190602401602060405180830381865afa158015610908573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061092c9190611f25565b9050838110156040518060400160405280601f81526020017f353130317c436f6d6d6f6e3a20696e73756666696369656e742066756e647300815250906109865760405162461bcd60e51b81526004016106a29190611ef3565b50600a84905560405163f89db45760e01b8152600481018690526001600160a01b0383169063f89db457906024015f604051808303815f87803b1580156109cb575f80fd5b505af11580156109dd573d5f803e3d5ffd5b50505050600a545f146040518060600160405280602e8152602001612119602e913990610a1d5760405162461bcd60e51b81526004016106a29190611ef3565b5084866001600160a01b03167fa5411d116afcb8415f0019792cccf595f94852ba82886dacbf756d9c3ce85f3f86604051610a5a91815260200190565b60405180910390a3505050505050565b5f80516020612049833981519152610a81816116d5565b600580546001600160a01b0319166001600160a01b0384169081179091556040519081527fccbdeb71dc680b2f0fd85f93e7ef0f70eda32a12b918c9715be07bb646360e599060200161064e565b5f8051602061216e833981519152610ae6816116d5565b5f610af088611495565b90505f60075487610b019190611f50565b6040516334284dfb60e01b8152600481018a9052602481018290526001600160801b03198816604482015261ffff8716606482015285151560848201529091506001600160a01b038316906334284dfb9060a4015f604051808303815f87803b158015610b6c575f80fd5b505af1158015610b7e573d5f803e3d5ffd5b5050505087896001600160a01b03167f92c1dbadbb7b791052341ce7c185ca37861e432438355dc3e1642b7c1287609a89600754604051610bc9929190918252602082015260400190565b60405180910390a3505050505050505050565b7f79af5a85184b45263c1b3721b9c8eb1becd98e55ade5a61da0fed18b90b83256610c06816116d5565b5f610c1084611495565b604051636df6c7d560e11b8152600481018590529091506001600160a01b0382169063dbed8faa906024015f604051808303815f87803b158015610c52575f80fd5b505af1158015610c64573d5f803e3d5ffd5b50505050836001600160a01b03167f466723bb873015d5baf515fce6b0c8df2cb154adfd8badb6bc77a620a6ef676184604051610ca391815260200190565b60405180910390a250505050565b5f80516020612049833981519152610cc8816116d5565b6283d60082106040518060600160405280602a815260200161201f602a913990610d055760405162461bcd60e51b81526004016106a29190611ef3565b5060088290556040518281527fdc10143650bb79cd7a92cdf792545dcc2c3b0a719bebb3d83a8a21bb8fbfc3d69060200161064e565b7f581be84b4822abe392a3bcac0d4d656e4cd57faf2e321a32108215cdd4a83656610d65816116d5565b6040516306b2a38360e41b8152600481018390526001600160a01b03841690636b2a3830906024015f604051808303815f87803b158015610da4575f80fd5b505af1158015610db6573d5f803e3d5ffd5b50505050826001600160a01b03167fcc1685e553848099cad0e11271e780371c44b5ed92a54552fbedf9f1b6b9f2478360405161083a91815260200190565b5f828152600160205260408120610e0c908361183d565b9392505050565b5f918252602082815260408084206001600160a01b0393909316845291905290205460ff1690565b7f6ac2c8b5322a929d300af17766189693ff6db6939ffe690430ad4720baf47d1c610e65816116d5565b5f610e6f86611495565b90505f60075484610e809190611f50565b6040516304e7595f60e21b81526004810188905260248101879052604481018290529091506001600160a01b0383169063139d657c906064015f604051808303815f87803b158015610ed0575f80fd5b505af1158015610ee2573d5f803e3d5ffd5b505050508486886001600160a01b03167fcb06dfb173e98cd60a8931644d288167e193c33d81c1019b9f9aab36b13fbc0387600754604051610f2e929190918252602082015260400190565b60405180910390a450505050505050565b5f8051602061216e833981519152610f56816116d5565b5f610f6086611495565b90505f60075485610f719190611f50565b6040516320199e7960e21b8152600481018890526024810182905285151560448201529091506001600160a01b0383169063806679e4906064015f604051808303815f87803b158015610fc2575f80fd5b505af1158015610fd4573d5f803e3d5ffd5b5050505085876001600160a01b03167f92c1dbadbb7b791052341ce7c185ca37861e432438355dc3e1642b7c1287609a8760075460405161101f929190918252602082015260400190565b60405180910390a350505050505050565b5f8181526001602052604081206105e790611848565b5f82815260208190526040902060010154611060816116d5565b6106f68383611812565b5f80516020612049833981519152611081816116d5565b610e10821060405180606001604052806027815260200161214760279139906110bd5760405162461bcd60e51b81526004016106a29190611ef3565b5060098290556040518281527f7f63f876249f25f7856e620802edd09af38a83c9c6040fcdb3717915403083119060200161064e565b5f805160206120f983398151915261110a816116d5565b5f61111484611495565b604051631902cad960e31b8152600481018590529091505f906001600160a01b0383169063c81656c890602401602060405180830381865afa15801561115c573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906111809190611f25565b6003546040516370a0823160e01b81526001600160a01b0385811660048301529293505f92909116906370a0823190602401602060405180830381865afa1580156111cd573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906111f19190611f25565b90505f82116040518060600160405280602781526020016120d2602791399061122d5760405162461bcd60e51b81526004016106a29190611ef3565b5060408051808201909152601f81527f353130317c436f6d6d6f6e3a20696e73756666696369656e742066756e6473006020820152828210156112835760405162461bcd60e51b81526004016106a29190611ef3565b50600a82905560405163e3dbffd560e01b8152600481018690526001600160a01b0384169063e3dbffd5906024015f604051808303815f87803b1580156112c8575f80fd5b505af11580156112da573d5f803e3d5ffd5b50505050600a545f146040518060600160405280602e8152602001612119602e91399061131a5760405162461bcd60e51b81526004016106a29190611ef3565b5084866001600160a01b03167fdfab01fc691e8b69fca482c65142d1bcfeafeb9eb6e9fd5220867d7196d9476e84604051610a5a91815260200190565b7f56d36dba8e2d5c88fc1038bd95811d491e67c9a66c6848c9d4c9309b9587e064611381816116d5565b61138a826116e2565b306001600160a01b0316826001600160a01b031663ce1b1d436040518163ffffffff1660e01b8152600401602060405180830381865afa1580156113d0573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906113f49190611f63565b6001600160a01b0316146040518060600160405280603281526020016120a060329139906114355760405162461bcd60e51b81526004016106a29190611ef3565b506001600160a01b038381165f8181526004602090815260409182902080546001600160a01b0319169487169485179055905192835290917f0c1464e8c817204497a2076f3b466fe64093e83033c83303b0cb44001cd9b6b5910161083a565b6001600160a01b038082165f90815260046020908152604080832054815160608101909252602f8083529394169283151592611ff090830139906114ec5760405162461bcd60e51b81526004016106a29190611ef3565b5092915050565b5f8051602061216e83398151915261150a816116d5565b5f61151485611495565b90505f831561152557600754611527565b5f5b90505f6115348286611f50565b60405163e4d7ddb960e01b815260048101889052602481018290529091506001600160a01b0384169063e4d7ddb9906044015f604051808303815f87803b15801561157d575f80fd5b505af115801561158f573d5f803e3d5ffd5b505060408051888152602081018690528993506001600160a01b038b1692507f8ba3ad1bd4ff5c3d75861d56df66b7942244d700f4d4cb67df693bac78e805a2910161101f565b5f805160206120f98339815191526115ed816116d5565b5f6115f785611495565b60405163456575e560e01b815260048101869052602481018590529091506001600160a01b0382169063456575e5906044015f604051808303815f87803b158015611640575f80fd5b505af1158015611652573d5f803e3d5ffd5b5050505083856001600160a01b03167f6f60b80b8dcecc3741c03485554a9a35ced96ebba66405fd324caf12a16261da8560405161169291815260200190565b60405180910390a35050505050565b5f6001600160e01b03198216637965db0b60e01b14806105e757506301ffc9a760e01b6001600160e01b03198316146105e7565b6116df8133611851565b50565b6002546040516385bb392360e01b81526001600160a01b038381166004830152909116906385bb392390602401602060405180830381865afa15801561172a573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061174e9190611f7e565b60405180606001604052806037815260200161206960379139906106ce5760405162461bcd60e51b81526004016106a29190611ef3565b604080516001600160a01b0385811660248301528416604482015260648082018490528251808303909101815260849091019091526020810180516001600160e01b03166323b872dd60e01b1790526106f690859061188a565b5f806117eb84846118eb565b90508015610e0c575f84815260016020526040902061180a908461197a565b509392505050565b5f8061181e848461198e565b90508015610e0c575f84815260016020526040902061180a90846119f7565b5f610e0c8383611a0b565b5f6105e7825490565b61185b8282610e13565b6106ce5760405163e2517d3f60e01b81526001600160a01b0382166004820152602481018390526044016106a2565b5f61189e6001600160a01b03841683611a31565b905080515f141580156118c25750808060200190518101906118c09190611f7e565b155b1561072f57604051635274afe760e01b81526001600160a01b03841660048201526024016106a2565b5f6118f68383610e13565b611973575f838152602081815260408083206001600160a01b03861684529091529020805460ff1916600117905561192b3390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45060016105e7565b505f6105e7565b5f610e0c836001600160a01b038416611a3e565b5f6119998383610e13565b15611973575f838152602081815260408083206001600160a01b0386168085529252808320805460ff1916905551339286917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a45060016105e7565b5f610e0c836001600160a01b038416611a83565b5f825f018281548110611a2057611a20611f99565b905f5260205f200154905092915050565b6060610e0c83835f611b66565b5f81815260018301602052604081205461197357508154600181810184555f8481526020808220909301849055845484825282860190935260409020919091556105e7565b5f8181526001830160205260408120548015611b5d575f611aa5600183611fad565b85549091505f90611ab890600190611fad565b9050808214611b17575f865f018281548110611ad657611ad6611f99565b905f5260205f200154905080875f018481548110611af657611af6611f99565b5f918252602080832090910192909255918252600188019052604090208390555b8554869080611b2857611b28611fc0565b600190038181905f5260205f20015f90559055856001015f8681526020019081526020015f205f9055600193505050506105e7565b5f9150506105e7565b606081471015611b8b5760405163cd78605960e01b81523060048201526024016106a2565b5f80856001600160a01b03168486604051611ba69190611fd4565b5f6040518083038185875af1925050503d805f8114611be0576040519150601f19603f3d011682016040523d82523d5f602084013e611be5565b606091505b5091509150611bf5868383611bff565b9695505050505050565b606082611c1457611c0f82611c5b565b610e0c565b8151158015611c2b57506001600160a01b0384163b155b15611c5457604051639996b31560e01b81526001600160a01b03851660048201526024016106a2565b5080610e0c565b805115611c6b5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b5f60208284031215611c94575f80fd5b81356001600160e01b031981168114610e0c575f80fd5b6001600160a01b03811681146116df575f80fd5b5f60208284031215611ccf575f80fd5b8135610e0c81611cab565b5f60208284031215611cea575f80fd5b5035919050565b5f8060408385031215611d02575f80fd5b823591506020830135611d1481611cab565b809150509250929050565b5f8060408385031215611d30575f80fd5b8235611d3b81611cab565b946020939093013593505050565b5f805f60608486031215611d5b575f80fd5b8335611d6681611cab565b95602085013595506040909401359392505050565b80151581146116df575f80fd5b5f805f805f8060c08789031215611d9d575f80fd5b8635611da881611cab565b9550602087013594506040870135935060608701356001600160801b031981168114611dd2575f80fd5b9250608087013561ffff81168114611de8575f80fd5b915060a0870135611df881611d7b565b809150509295509295509295565b5f8060408385031215611e17575f80fd5b50508035926020909101359150565b5f805f8060808587031215611e39575f80fd5b8435611e4481611cab565b966020860135965060408601359560600135945092505050565b5f805f8060808587031215611e71575f80fd5b8435611e7c81611cab565b935060208501359250604085013591506060850135611e9a81611d7b565b939692955090935050565b5f8060408385031215611eb6575f80fd5b8235611ec181611cab565b91506020830135611d1481611cab565b5f5b83811015611eeb578181015183820152602001611ed3565b50505f910152565b602081525f8251806020840152611f11816040850160208701611ed1565b601f01601f19169190910160400192915050565b5f60208284031215611f35575f80fd5b5051919050565b634e487b7160e01b5f52601160045260245ffd5b808201808211156105e7576105e7611f3c565b5f60208284031215611f73575f80fd5b8151610e0c81611cab565b5f60208284031215611f8e575f80fd5b8151610e0c81611d7b565b634e487b7160e01b5f52603260045260245ffd5b818103818111156105e7576105e7611f3c565b634e487b7160e01b5f52603160045260245ffd5b5f8251611fe5818460208701611ed1565b919091019291505056fe353230327c50726f636573736f723a206e6f207061796d656e74206163636f756e7420666f72207468652063617264353231307c50726f636573736f723a20736574746c656d656e7420706572696f6420746f6f206c6f6e67fa658e47c75a6eb0e27156b2f72e1b295c98b53e8a1d238d7ca6ea6345b34880353230307c50726f636573736f723a207061796d656e74206163636f756e74206e6f74206465706c6f79656420627920666163746f7279353230317c50726f636573736f723a207061796d656e74206163636f756e742070726f636573736f72206d69736d61746368353235307c50726f636573736f723a20726566756e64207265636f7264206e6f7420666f756e64d77cc12a543481a2b3ef8fd055979569715a10db9879cd9e664395eca3a54dff353235327c50726f636573736f723a20726566756e6420746f2070726f63657373207761736e2774207265736574353231317c50726f636573736f723a2073656375726974792064656c617920746f6f206c6f6e67a567244cad934c87b7f7e15b15dd0e75f95a76afd1916f8eab4a391410d6f278353235317c50726f636573736f723a20726566756e6420746f2070726f63657373206973207a65726fa2646970667358221220abcbacdb9048a2744f99cdd5031db80dd0390bf503ccade142542ce178f0668364736f6c63430008160033"; + public static final String BINARY = "608060405262278d006008553480156200001857600080fd5b506040516200272f3803806200272f8339810160408190526200003b9162000214565b60028054600580546001600160a01b03199081166001600160a01b038a8116919091179092556006805482168984161790556001600160a81b03199092166101008783160217909255600380549091169184169190911790556009819055620000a6600033620000b2565b5050505050506200027b565b600080620000c18484620000ef565b90508015620000e6576000848152600160205260409020620000e490846200019d565b505b90505b92915050565b6000828152602081815260408083206001600160a01b038516845290915281205460ff1662000194576000838152602081815260408083206001600160a01b03861684529091529020805460ff191660011790556200014b3390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a4506001620000e9565b506000620000e9565b6000620000e6836001600160a01b03841660008181526001830160205260408120546200019457508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155620000e9565b80516001600160a01b03811681146200020f57600080fd5b919050565b600080600080600060a086880312156200022d57600080fd5b6200023886620001f7565b94506200024860208701620001f7565b93506200025860408701620001f7565b92506200026860608701620001f7565b9150608086015190509295509295909350565b6124a4806200028b6000396000f3fe608060405234801561001057600080fd5b506004361061028a5760003560e01c806383af133d1161015c578063c904ea39116100ce578063da90f3a011610087578063da90f3a0146105f1578063e24fd1f114610604578063e452f20d14610617578063e63ab1e91461062a578063e96ede8e14610651578063fe2208aa1461066457600080fd5b8063c904ea3914610589578063ca15c8731461059c578063cb37f3b2146105af578063d47ae89c146105c2578063d547741f146105cb578063d6d32b2a146105de57600080fd5b80639353ce4c116101205780639353ce4c146104f357806396bc563d1461051a578063978975591461052f578063a217fddf14610556578063b77c6d351461055e578063be4d18511461057657600080fd5b806383af133d146104a95780638456cb59146104bc5780639010d07c146104c457806391792d5b146104d757806391d14854146104e057600080fd5b80633f4ba83a11610200578063601c6065116101b9578063601c60651461042357806365ebf99a1461043657806370e4306e1461044957806373d3467c146104705780637f2fadb0146104835780638072a0221461049657600080fd5b80633f4ba83a146103ae5780634413e098146103b65780634b9bb0b4146103c95780634e6d8a73146103f057806356737951146104035780635c975abb1461041857600080fd5b80632f2ff15d116102525780632f2ff15d1461030e5780633013ce291461032157806336568abe1461034c57806337de81061461035f5780633d409a86146103725780633e1dbe1f1461038757600080fd5b806301ffc9a71461028f5780630f1071be146102b757806311dce771146102ce578063248a9ca3146102e35780632cc3264114610306575b600080fd5b6102a261029d366004611f47565b610677565b60405190151581526020015b60405180910390f35b6102c060085481565b6040519081526020016102ae565b6102e16102dc366004611f86565b6106a2565b005b6102c06102f1366004611fa3565b60009081526020819052604090206001015490565b6102e1610710565b6102e161031c366004611fbc565b61078a565b600354610334906001600160a01b031681565b6040516001600160a01b0390911681526020016102ae565b6102e161035a366004611fbc565b6107b5565b6102e161036d366004611fa3565b6107ed565b6102c060008051602061242683398151915281565b6102c07f80578ee961742bd59ed4d7f52c856d4014970acf79709a07f09c8c279d66cbb481565b6102e161084c565b6102e16103c4366004611fec565b610881565b6102c07fc62d23620f0c241b20cdec293f57ff9c0cc09ac7c55352efe3363184cd93032281565b6102e16103fe366004612018565b61094d565b6102c06000805160206123b183398151915281565b60025460ff166102a2565b6102e1610431366004612018565b610a57565b6102e1610444366004611f86565b610c7f565b6102c07f6ac2c8b5322a929d300af17766189693ff6db6939ffe690430ad4720baf47d1c81565b6102e161047e36600461205b565b610ce5565b6102e1610491366004611fec565b610e02565b6102e16104a4366004611fa3565b610ee5565b6102e16104b7366004611fec565b610f70565b6102e161102f565b6103346104d23660046120df565b611061565b6102c060075481565b6102a26104ee366004611fbc565b611080565b6102c07f79af5a85184b45263c1b3721b9c8eb1becd98e55ade5a61da0fed18b90b8325681565b6102c060008051602061230183398151915281565b6102c07f581be84b4822abe392a3bcac0d4d656e4cd57faf2e321a32108215cdd4a8365681565b6102c0600081565b6002546103349061010090046001600160a01b031681565b600654610334906001600160a01b031681565b6102e1610597366004612101565b6110a9565b6102c06105aa366004611fa3565b6111aa565b600554610334906001600160a01b031681565b6102c060095481565b6102e16105d9366004611fbc565b6111c1565b6102e16105ec366004611fa3565b6111e6565b6102e16105ff366004611fec565b611270565b6102e161061236600461214b565b6114ed565b610334610625366004611f86565b61162e565b6102c07f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a81565b6102e161065f366004612018565b61168d565b6102e1610672366004612018565b611782565b60006001600160e01b03198216635a05180f60e01b148061069c575061069c8261185c565b92915050565b6000805160206123018339815191526106ba81611891565b600680546001600160a01b0319166001600160a01b0384169081179091556040519081527f68c7b435e0ad7bfb2140fab0735300efbda206f62906049e0b35763cef67cc5e906020015b60405180910390a15050565b600a54339061071e8261189b565b6000811160405180606001604052806029815260200161244660299139906107625760405162461bcd60e51b8152600401610759919061219d565b60405180910390fd5b506000600a55600654600354610786916001600160a01b0391821691168484611946565b5050565b6000828152602081905260409020600101546107a581611891565b6107af83836119a0565b50505050565b6001600160a01b03811633146107de5760405163334bd91960e11b815260040160405180910390fd5b6107e882826119d5565b505050565b7f80578ee961742bd59ed4d7f52c856d4014970acf79709a07f09c8c279d66cbb461081781611891565b60078290556040518281527fb5aa183eb20407e22587bcd13d5c82a85835a4bd60e2a13d7c7efee3c2e9ed4890602001610704565b7f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a61087681611891565b61087e611a02565b50565b7f581be84b4822abe392a3bcac0d4d656e4cd57faf2e321a32108215cdd4a836566108ab81611891565b60405163b9603bdf60e01b8152600481018390526001600160a01b0384169063b9603bdf90602401600060405180830381600087803b1580156108ed57600080fd5b505af1158015610901573d6000803e3d6000fd5b50505050826001600160a01b03167fc8818db5b3e4e986e2f21e002090b1513e17c05a7a326c3737af624bf6ef78c48360405161094091815260200190565b60405180910390a2505050565b610955611a54565b7f6ac2c8b5322a929d300af17766189693ff6db6939ffe690430ad4720baf47d1c61097f81611891565b600061098a8561162e565b905060006007548461099c91906121e6565b604051630381b4dd60e01b815260048101879052602481018290529091506001600160a01b03831690630381b4dd90604401600060405180830381600087803b1580156109e857600080fd5b505af11580156109fc573d6000803e3d6000fd5b5050505084866001600160a01b03167fa77a4ae2cfa0139350d70cd52434d01ae72e9ec897ae89be03580f1ff00dd3d086600754604051610a47929190918252602082015260400190565b60405180910390a3505050505050565b610a5f611a54565b6000805160206123b1833981519152610a7781611891565b60408051808201909152601b81527f353130307c436f6d6d6f6e3a20616d6f756e74206973207a65726f0000000000602082015282610ac95760405162461bcd60e51b8152600401610759919061219d565b506000610ad58561162e565b6003546006546040516370a0823160e01b81526001600160a01b0391821660048201529293506000929116906370a0823190602401602060405180830381865afa158015610b27573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b4b91906121f9565b9050838110156040518060400160405280601f81526020017f353130317c436f6d6d6f6e3a20696e73756666696369656e742066756e64730081525090610ba55760405162461bcd60e51b8152600401610759919061219d565b50600a84905560405163f89db45760e01b8152600481018690526001600160a01b0383169063f89db45790602401600060405180830381600087803b158015610bed57600080fd5b505af1158015610c01573d6000803e3d6000fd5b50505050600a546000146040518060600160405280602e81526020016123d1602e913990610c425760405162461bcd60e51b8152600401610759919061219d565b5084866001600160a01b03167fa5411d116afcb8415f0019792cccf595f94852ba82886dacbf756d9c3ce85f3f86604051610a4791815260200190565b600080516020612301833981519152610c9781611891565b600580546001600160a01b0319166001600160a01b0384169081179091556040519081527fccbdeb71dc680b2f0fd85f93e7ef0f70eda32a12b918c9715be07bb646360e5990602001610704565b610ced611a54565b600080516020612426833981519152610d0581611891565b6000610d108861162e565b9050600060075487610d2291906121e6565b6040516334284dfb60e01b8152600481018a9052602481018290526001600160801b03198816604482015261ffff8716606482015285151560848201529091506001600160a01b038316906334284dfb9060a401600060405180830381600087803b158015610d9057600080fd5b505af1158015610da4573d6000803e3d6000fd5b5050505087896001600160a01b03167f92c1dbadbb7b791052341ce7c185ca37861e432438355dc3e1642b7c1287609a89600754604051610def929190918252602082015260400190565b60405180910390a3505050505050505050565b610e0a611a54565b7f79af5a85184b45263c1b3721b9c8eb1becd98e55ade5a61da0fed18b90b83256610e3481611891565b6000610e3f8461162e565b604051636df6c7d560e11b8152600481018590529091506001600160a01b0382169063dbed8faa90602401600060405180830381600087803b158015610e8457600080fd5b505af1158015610e98573d6000803e3d6000fd5b50505050836001600160a01b03167f466723bb873015d5baf515fce6b0c8df2cb154adfd8badb6bc77a620a6ef676184604051610ed791815260200190565b60405180910390a250505050565b600080516020612301833981519152610efd81611891565b6283d60082106040518060600160405280602a81526020016122d7602a913990610f3a5760405162461bcd60e51b8152600401610759919061219d565b5060088290556040518281527fdc10143650bb79cd7a92cdf792545dcc2c3b0a719bebb3d83a8a21bb8fbfc3d690602001610704565b7f581be84b4822abe392a3bcac0d4d656e4cd57faf2e321a32108215cdd4a83656610f9a81611891565b6040516306b2a38360e41b8152600481018390526001600160a01b03841690636b2a383090602401600060405180830381600087803b158015610fdc57600080fd5b505af1158015610ff0573d6000803e3d6000fd5b50505050826001600160a01b03167fcc1685e553848099cad0e11271e780371c44b5ed92a54552fbedf9f1b6b9f2478360405161094091815260200190565b7f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a61105981611891565b61087e611a7a565b60008281526001602052604081206110799083611ab7565b9392505050565b6000918252602082815260408084206001600160a01b0393909316845291905290205460ff1690565b6110b1611a54565b6000805160206124268339815191526110c981611891565b60006110d48661162e565b90506000600754856110e691906121e6565b6040516320199e7960e21b8152600481018890526024810182905285151560448201529091506001600160a01b0383169063806679e490606401600060405180830381600087803b15801561113a57600080fd5b505af115801561114e573d6000803e3d6000fd5b5050505085876001600160a01b03167f92c1dbadbb7b791052341ce7c185ca37861e432438355dc3e1642b7c1287609a87600754604051611199929190918252602082015260400190565b60405180910390a350505050505050565b600081815260016020526040812061069c90611ac3565b6000828152602081905260409020600101546111dc81611891565b6107af83836119d5565b6000805160206123018339815191526111fe81611891565b610e1082106040518060600160405280602781526020016123ff602791399061123a5760405162461bcd60e51b8152600401610759919061219d565b5060098290556040518281527f7f63f876249f25f7856e620802edd09af38a83c9c6040fcdb37179154030831190602001610704565b611278611a54565b6000805160206123b183398151915261129081611891565b600061129b8461162e565b604051631902cad960e31b8152600481018590529091506000906001600160a01b0383169063c81656c890602401602060405180830381865afa1580156112e6573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061130a91906121f9565b6003546006546040516370a0823160e01b81526001600160a01b0391821660048201529293506000929116906370a0823190602401602060405180830381865afa15801561135c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061138091906121f9565b90506000821160405180606001604052806027815260200161238a60279139906113bd5760405162461bcd60e51b8152600401610759919061219d565b5060408051808201909152601f81527f353130317c436f6d6d6f6e3a20696e73756666696369656e742066756e6473006020820152828210156114135760405162461bcd60e51b8152600401610759919061219d565b50600a82905560405163e3dbffd560e01b8152600481018690526001600160a01b0384169063e3dbffd590602401600060405180830381600087803b15801561145b57600080fd5b505af115801561146f573d6000803e3d6000fd5b50505050600a546000146040518060600160405280602e81526020016123d1602e9139906114b05760405162461bcd60e51b8152600401610759919061219d565b5084866001600160a01b03167fdfab01fc691e8b69fca482c65142d1bcfeafeb9eb6e9fd5220867d7196d9476e84604051610a4791815260200190565b7fc62d23620f0c241b20cdec293f57ff9c0cc09ac7c55352efe3363184cd93032261151781611891565b6115208261189b565b306001600160a01b0316826001600160a01b031663ce1b1d436040518163ffffffff1660e01b8152600401602060405180830381865afa158015611568573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061158c9190612212565b6001600160a01b03161460405180606001604052806032815260200161235860329139906115cd5760405162461bcd60e51b8152600401610759919061219d565b506001600160a01b0383811660008181526004602090815260409182902080546001600160a01b0319169487169485179055905192835290917f0c1464e8c817204497a2076f3b466fe64093e83033c83303b0cb44001cd9b6b59101610940565b6001600160a01b03808216600090815260046020908152604080832054815160608101909252602f80835293941692831515926122a890830139906116865760405162461bcd60e51b8152600401610759919061219d565b5092915050565b611695611a54565b6000805160206124268339815191526116ad81611891565b60006116b88561162e565b9050600083156116ca576007546116cd565b60005b905060006116db82866121e6565b60405163e4d7ddb960e01b815260048101889052602481018290529091506001600160a01b0384169063e4d7ddb990604401600060405180830381600087803b15801561172757600080fd5b505af115801561173b573d6000803e3d6000fd5b505060408051888152602081018690528993506001600160a01b038b1692507f8ba3ad1bd4ff5c3d75861d56df66b7942244d700f4d4cb67df693bac78e805a29101611199565b61178a611a54565b6000805160206123b18339815191526117a281611891565b60006117ad8561162e565b60405163456575e560e01b815260048101869052602481018590529091506001600160a01b0382169063456575e590604401600060405180830381600087803b1580156117f957600080fd5b505af115801561180d573d6000803e3d6000fd5b5050505083856001600160a01b03167f6f60b80b8dcecc3741c03485554a9a35ced96ebba66405fd324caf12a16261da8560405161184d91815260200190565b60405180910390a35050505050565b60006001600160e01b03198216637965db0b60e01b148061069c57506301ffc9a760e01b6001600160e01b031983161461069c565b61087e8133611acd565b6002546040516385bb392360e01b81526001600160a01b038381166004830152610100909204909116906385bb392390602401602060405180830381865afa1580156118eb573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061190f919061222f565b60405180606001604052806037815260200161232160379139906107865760405162461bcd60e51b8152600401610759919061219d565b604080516001600160a01b0385811660248301528416604482015260648082018490528251808303909101815260849091019091526020810180516001600160e01b03166323b872dd60e01b1790526107af908590611b06565b6000806119ad8484611b69565b905080156110795760008481526001602052604090206119cd9084611bfb565b509392505050565b6000806119e28484611c10565b905080156110795760008481526001602052604090206119cd9084611c7b565b611a0a611c90565b6002805460ff191690557f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b6040516001600160a01b03909116815260200160405180910390a1565b60025460ff1615611a785760405163d93c066560e01b815260040160405180910390fd5b565b611a82611a54565b6002805460ff191660011790557f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a258611a373390565b60006110798383611cb3565b600061069c825490565b611ad78282611080565b6107865760405163e2517d3f60e01b81526001600160a01b038216600482015260248101839052604401610759565b6000611b1b6001600160a01b03841683611cdd565b90508051600014158015611b40575080806020019051810190611b3e919061222f565b155b156107e857604051635274afe760e01b81526001600160a01b0384166004820152602401610759565b6000611b758383611080565b611bf3576000838152602081815260408083206001600160a01b03861684529091529020805460ff19166001179055611bab3390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a450600161069c565b50600061069c565b6000611079836001600160a01b038416611ceb565b6000611c1c8383611080565b15611bf3576000838152602081815260408083206001600160a01b0386168085529252808320805460ff1916905551339286917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a450600161069c565b6000611079836001600160a01b038416611d32565b60025460ff16611a7857604051638dfc202b60e01b815260040160405180910390fd5b6000826000018281548110611cca57611cca61224c565b9060005260206000200154905092915050565b606061107983836000611e25565b6000818152600183016020526040812054611bf35750815460018181018455600084815260208082209093018490558454848252828601909352604090209190915561069c565b60008181526001830160205260408120548015611e1b576000611d56600183612262565b8554909150600090611d6a90600190612262565b9050808214611dcf576000866000018281548110611d8a57611d8a61224c565b9060005260206000200154905080876000018481548110611dad57611dad61224c565b6000918252602080832090910192909255918252600188019052604090208390555b8554869080611de057611de0612275565b60019003818190600052602060002001600090559055856001016000868152602001908152602001600020600090556001935050505061069c565b600091505061069c565b606081471015611e4a5760405163cd78605960e01b8152306004820152602401610759565b600080856001600160a01b03168486604051611e66919061228b565b60006040518083038185875af1925050503d8060008114611ea3576040519150601f19603f3d011682016040523d82523d6000602084013e611ea8565b606091505b5091509150611eb8868383611ec2565b9695505050505050565b606082611ed757611ed282611f1e565b611079565b8151158015611eee57506001600160a01b0384163b155b15611f1757604051639996b31560e01b81526001600160a01b0385166004820152602401610759565b5080611079565b805115611f2e5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b600060208284031215611f5957600080fd5b81356001600160e01b03198116811461107957600080fd5b6001600160a01b038116811461087e57600080fd5b600060208284031215611f9857600080fd5b813561107981611f71565b600060208284031215611fb557600080fd5b5035919050565b60008060408385031215611fcf57600080fd5b823591506020830135611fe181611f71565b809150509250929050565b60008060408385031215611fff57600080fd5b823561200a81611f71565b946020939093013593505050565b60008060006060848603121561202d57600080fd5b833561203881611f71565b95602085013595506040909401359392505050565b801515811461087e57600080fd5b60008060008060008060c0878903121561207457600080fd5b863561207f81611f71565b9550602087013594506040870135935060608701356001600160801b0319811681146120aa57600080fd5b9250608087013561ffff811681146120c157600080fd5b915060a08701356120d18161204d565b809150509295509295509295565b600080604083850312156120f257600080fd5b50508035926020909101359150565b6000806000806080858703121561211757600080fd5b843561212281611f71565b9350602085013592506040850135915060608501356121408161204d565b939692955090935050565b6000806040838503121561215e57600080fd5b823561216981611f71565b91506020830135611fe181611f71565b60005b8381101561219457818101518382015260200161217c565b50506000910152565b60208152600082518060208401526121bc816040850160208701612179565b601f01601f19169190910160400192915050565b634e487b7160e01b600052601160045260246000fd5b8082018082111561069c5761069c6121d0565b60006020828403121561220b57600080fd5b5051919050565b60006020828403121561222457600080fd5b815161107981611f71565b60006020828403121561224157600080fd5b81516110798161204d565b634e487b7160e01b600052603260045260246000fd5b8181038181111561069c5761069c6121d0565b634e487b7160e01b600052603160045260246000fd5b6000825161229d818460208701612179565b919091019291505056fe353230327c50726f636573736f723a206e6f207061796d656e74206163636f756e7420666f72207468652063617264353231307c50726f636573736f723a20736574746c656d656e7420706572696f6420746f6f206c6f6e67fa658e47c75a6eb0e27156b2f72e1b295c98b53e8a1d238d7ca6ea6345b34880353230307c50726f636573736f723a207061796d656e74206163636f756e74206e6f74206465706c6f79656420627920666163746f7279353230317c50726f636573736f723a207061796d656e74206163636f756e742070726f636573736f72206d69736d61746368353235307c50726f636573736f723a20726566756e64207265636f7264206e6f7420666f756e64d77cc12a543481a2b3ef8fd055979569715a10db9879cd9e664395eca3a54dff353235327c50726f636573736f723a20726566756e6420746f2070726f63657373207761736e2774207265736574353231317c50726f636573736f723a2073656375726974792064656c617920746f6f206c6f6e67a567244cad934c87b7f7e15b15dd0e75f95a76afd1916f8eab4a391410d6f278353235317c50726f636573736f723a20726566756e6420746f2070726f63657373206973207a65726fa2646970667358221220098c0e97a9ad9d3bb5cd4459c44925a6fb83295823a48d337beb3c254c7b728b64736f6c63430008160033"; public static final String FUNC_AUTHORIZATION_PROCESSOR_ROLE = "AUTHORIZATION_PROCESSOR_ROLE"; @@ -52,7 +49,11 @@ class TangemBridgeProcessor extends Contract { public static final String FUNC_DEFAULT_ADMIN_ROLE = "DEFAULT_ADMIN_ROLE"; - public static final String FUNC_PAYMENT_ACCOUNT_SETTER = "PAYMENT_ACCOUNT_SETTER"; + public static final String FUNC_FIXED_FEE_SETTER_ROLE = "FIXED_FEE_SETTER_ROLE"; + + public static final String FUNC_PAUSER_ROLE = "PAUSER_ROLE"; + + public static final String FUNC_PAYMENT_ACCOUNT_SETTER_ROLE = "PAYMENT_ACCOUNT_SETTER_ROLE"; public static final String FUNC_PROPERTY_SETTER_ROLE = "PROPERTY_SETTER_ROLE"; @@ -76,6 +77,10 @@ class TangemBridgeProcessor extends Contract { public static final String FUNC_INCREASEVERIFIEDBALANCEFOR = "increaseVerifiedBalanceFor"; + public static final String FUNC_PAUSE = "pause"; + + public static final String FUNC_PAUSED = "paused"; + public static final String FUNC_PAYMENTACCOUNTFACTORY = "paymentAccountFactory"; public static final String FUNC_PAYMENTRECEIVER = "paymentReceiver"; @@ -124,6 +129,8 @@ class TangemBridgeProcessor extends Contract { public static final String FUNC_SUPPORTSINTERFACE = "supportsInterface"; + public static final String FUNC_UNPAUSE = "unpause"; + public static final String FUNC_WRITEOFFDEBT = "writeOffDebt"; public static final Event AUTHORIZATIONCHANGEPROCESSED_EVENT = new Event("AuthorizationChangeProcessed", @@ -149,6 +156,10 @@ class TangemBridgeProcessor extends Contract { List.of(new TypeReference() { })); + public static final Event PAUSED_EVENT = new Event("Paused", + List.of(new TypeReference

() { + })); + public static final Event PAYMENTACCOUNTSET_EVENT = new Event("PaymentAccountSet", Arrays.asList(new TypeReference
(true) { }, new TypeReference
() { @@ -209,11 +220,14 @@ class TangemBridgeProcessor extends Contract { public static final Event SETTLEMENTPROCESSED_EVENT = new Event("SettlementProcessed", Arrays.asList(new TypeReference
(true) { }, new TypeReference(true) { - }, new TypeReference(true) { }, new TypeReference() { }, new TypeReference() { })); + public static final Event UNPAUSED_EVENT = new Event("Unpaused", + List.of(new TypeReference
() { + })); + public static final Event VERIFIEDBALANCEINCREASEDFOR_EVENT = new Event("VerifiedBalanceIncreasedFor", Arrays.asList(new TypeReference
(true) { }, new TypeReference() { @@ -268,6 +282,26 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } + public static AuthorizationProcessedEventResponse getAuthorizationProcessedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(AUTHORIZATIONPROCESSED_EVENT, log); + AuthorizationProcessedEventResponse typedResponse = new AuthorizationProcessedEventResponse(); + typedResponse.log = log; + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.fee = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + return typedResponse; + } + + public static DebtWriteOffProcessedEventResponse getDebtWriteOffProcessedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTWRITEOFFPROCESSED_EVENT, log); + DebtWriteOffProcessedEventResponse typedResponse = new DebtWriteOffProcessedEventResponse(); + typedResponse.log = log; + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + return typedResponse; + } + public static List getAuthorizationProcessedEvents(TransactionReceipt transactionReceipt) { List valueList = staticExtractEventParametersWithLog(AUTHORIZATIONPROCESSED_EVENT, transactionReceipt); ArrayList responses = new ArrayList(valueList.size()); @@ -283,126 +317,6 @@ class TangemBridgeProcessor extends Contract { return responses; } - public static List getDebtWriteOffProcessedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(DEBTWRITEOFFPROCESSED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - DebtWriteOffProcessedEventResponse typedResponse = new DebtWriteOffProcessedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getFixedFeeSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(FIXEDFEESET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - FixedFeeSetEventResponse typedResponse = new FixedFeeSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.fixedFee = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static AuthorizationProcessedEventResponse getAuthorizationProcessedEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(AUTHORIZATIONPROCESSED_EVENT, log); - AuthorizationProcessedEventResponse typedResponse = new AuthorizationProcessedEventResponse(); - typedResponse.log = log; - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.fee = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - return typedResponse; - } - - public static List getPaymentAccountSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(PAYMENTACCOUNTSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - PaymentAccountSetEventResponse typedResponse = new PaymentAccountSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.paymentAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getPaymentReceiverSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(PAYMENTRECEIVERSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - PaymentReceiverSetEventResponse typedResponse = new PaymentReceiverSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.paymentReceiver = (String) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getPendingRefundProcessedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(PENDINGREFUNDPROCESSED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - PendingRefundProcessedEventResponse typedResponse = new PendingRefundProcessedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static DebtWriteOffProcessedEventResponse getDebtWriteOffProcessedEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTWRITEOFFPROCESSED_EVENT, log); - DebtWriteOffProcessedEventResponse typedResponse = new DebtWriteOffProcessedEventResponse(); - typedResponse.log = log; - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - return typedResponse; - } - - public static PendingRefundProcessedEventResponse getPendingRefundProcessedEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PENDINGREFUNDPROCESSED_EVENT, log); - PendingRefundProcessedEventResponse typedResponse = new PendingRefundProcessedEventResponse(); - typedResponse.log = log; - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - return typedResponse; - } - - public static List getRefundAccountSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(REFUNDACCOUNTSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - RefundAccountSetEventResponse typedResponse = new RefundAccountSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.refundAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getRefundProcessedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(REFUNDPROCESSED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - RefundProcessedEventResponse typedResponse = new RefundProcessedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - public static FixedFeeSetEventResponse getFixedFeeSetEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(FIXEDFEESET_EVENT, log); FixedFeeSetEventResponse typedResponse = new FixedFeeSetEventResponse(); @@ -411,43 +325,34 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } - public static List getRoleAdminChangedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(ROLEADMINCHANGED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); + public static List getPausedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAUSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); for (EventValuesWithLog eventValues : valueList) { - RoleAdminChangedEventResponse typedResponse = new RoleAdminChangedEventResponse(); + PausedEventResponse typedResponse = new PausedEventResponse(); typedResponse.log = eventValues.getLog(); - typedResponse.role = (byte[]) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.previousAdminRole = (byte[]) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.newAdminRole = (byte[]) eventValues.getIndexedValues().get(2).getValue(); + typedResponse.account = (String) eventValues.getNonIndexedValues().get(0).getValue(); responses.add(typedResponse); } return responses; } - public static List getRoleGrantedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(ROLEGRANTED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - RoleGrantedEventResponse typedResponse = new RoleGrantedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.role = (byte[]) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.account = (String) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.sender = (String) eventValues.getIndexedValues().get(2).getValue(); - responses.add(typedResponse); - } - return responses; + public static PausedEventResponse getPausedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PAUSED_EVENT, log); + PausedEventResponse typedResponse = new PausedEventResponse(); + typedResponse.log = log; + typedResponse.account = (String) eventValues.getNonIndexedValues().get(0).getValue(); + return typedResponse; } - public static List getRoleRevokedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(ROLEREVOKED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); + public static List getDebtWriteOffProcessedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(DEBTWRITEOFFPROCESSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); for (EventValuesWithLog eventValues : valueList) { - RoleRevokedEventResponse typedResponse = new RoleRevokedEventResponse(); + DebtWriteOffProcessedEventResponse typedResponse = new DebtWriteOffProcessedEventResponse(); typedResponse.log = eventValues.getLog(); - typedResponse.role = (byte[]) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.account = (String) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.sender = (String) eventValues.getIndexedValues().get(2).getValue(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); responses.add(typedResponse); } return responses; @@ -462,44 +367,6 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } - public static List getSaveRefundProcessedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(SAVEREFUNDPROCESSED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - SaveRefundProcessedEventResponse typedResponse = new SaveRefundProcessedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getSecurityDelaySetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(SECURITYDELAYSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - SecurityDelaySetEventResponse typedResponse = new SecurityDelaySetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.securityDelay = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getSettlementPeriodSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(SETTLEMENTPERIODSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - SettlementPeriodSetEventResponse typedResponse = new SettlementPeriodSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.settlementPeriod = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - public static PaymentReceiverSetEventResponse getPaymentReceiverSetEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PAYMENTRECEIVERSET_EVENT, log); PaymentReceiverSetEventResponse typedResponse = new PaymentReceiverSetEventResponse(); @@ -508,66 +375,6 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } - public static List getSettlementProcessedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(SETTLEMENTPROCESSED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - SettlementProcessedEventResponse typedResponse = new SettlementProcessedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.settlementId = (BigInteger) eventValues.getIndexedValues().get(2).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.fee = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getVerifiedBalanceIncreasedForEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCEINCREASEDFOR_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - VerifiedBalanceIncreasedForEventResponse typedResponse = new VerifiedBalanceIncreasedForEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.increase = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getVerifiedBalanceSetForEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCESETFOR_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - VerifiedBalanceSetForEventResponse typedResponse = new VerifiedBalanceSetForEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - @Deprecated - public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { - return new TangemBridgeProcessor(contractAddress, web3j, credentials, gasPrice, gasLimit); - } - - @Deprecated - public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { - return new TangemBridgeProcessor(contractAddress, web3j, transactionManager, gasPrice, gasLimit); - } - - public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { - return new TangemBridgeProcessor(contractAddress, web3j, credentials, contractGasProvider); - } - - public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { - return new TangemBridgeProcessor(contractAddress, web3j, transactionManager, contractGasProvider); - } - public static RefundAccountSetEventResponse getRefundAccountSetEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(REFUNDACCOUNTSET_EVENT, log); RefundAccountSetEventResponse typedResponse = new RefundAccountSetEventResponse(); @@ -576,32 +383,16 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } - public static RemoteCall deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { - String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), - new Address(160, refundAccount_), - new Address(160, paymentAccountFactory_), - new Address(160, paymentToken_), - new Uint256(securityDelay_))); - return deployRemoteCall(TangemBridgeProcessor.class, web3j, credentials, contractGasProvider, BINARY, encodedConstructor); - } - - public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { - String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), - new Address(160, refundAccount_), - new Address(160, paymentAccountFactory_), - new Address(160, paymentToken_), - new Uint256(securityDelay_))); - return deployRemoteCall(TangemBridgeProcessor.class, web3j, transactionManager, contractGasProvider, BINARY, encodedConstructor); - } - - @Deprecated - public static RemoteCall deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { - String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), - new Address(160, refundAccount_), - new Address(160, paymentAccountFactory_), - new Address(160, paymentToken_), - new Uint256(securityDelay_))); - return deployRemoteCall(TangemBridgeProcessor.class, web3j, credentials, gasPrice, gasLimit, BINARY, encodedConstructor); + public static List getFixedFeeSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(FIXEDFEESET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + FixedFeeSetEventResponse typedResponse = new FixedFeeSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.fixedFee = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; } public static RefundProcessedEventResponse getRefundProcessedEventFromLog(Log log) { @@ -614,16 +405,6 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } - @Deprecated - public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { - String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), - new Address(160, refundAccount_), - new Address(160, paymentAccountFactory_), - new Address(160, paymentToken_), - new Uint256(securityDelay_))); - return deployRemoteCall(TangemBridgeProcessor.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, encodedConstructor); - } - public static RoleAdminChangedEventResponse getRoleAdminChangedEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(ROLEADMINCHANGED_EVENT, log); RoleAdminChangedEventResponse typedResponse = new RoleAdminChangedEventResponse(); @@ -680,18 +461,62 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } + public static List getPaymentAccountSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAYMENTACCOUNTSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PaymentAccountSetEventResponse typedResponse = new PaymentAccountSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.paymentAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public static SettlementProcessedEventResponse getSettlementProcessedEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(SETTLEMENTPROCESSED_EVENT, log); SettlementProcessedEventResponse typedResponse = new SettlementProcessedEventResponse(); typedResponse.log = log; typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.settlementId = (BigInteger) eventValues.getIndexedValues().get(2).getValue(); typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); typedResponse.fee = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); return typedResponse; } + public static List getUnpausedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(UNPAUSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + UnpausedEventResponse typedResponse = new UnpausedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.account = (String) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static UnpausedEventResponse getUnpausedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(UNPAUSED_EVENT, log); + UnpausedEventResponse typedResponse = new UnpausedEventResponse(); + typedResponse.log = log; + typedResponse.account = (String) eventValues.getNonIndexedValues().get(0).getValue(); + return typedResponse; + } + + public static List getPaymentReceiverSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAYMENTRECEIVERSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PaymentReceiverSetEventResponse typedResponse = new PaymentReceiverSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentReceiver = (String) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public static VerifiedBalanceIncreasedForEventResponse getVerifiedBalanceIncreasedForEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(VERIFIEDBALANCEINCREASEDFOR_EVENT, log); VerifiedBalanceIncreasedForEventResponse typedResponse = new VerifiedBalanceIncreasedForEventResponse(); @@ -701,15 +526,112 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } - public static VerifiedBalanceSetForEventResponse getVerifiedBalanceSetForEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(VERIFIEDBALANCESETFOR_EVENT, log); - VerifiedBalanceSetForEventResponse typedResponse = new VerifiedBalanceSetForEventResponse(); + @Deprecated + public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + return new TangemBridgeProcessor(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + @Deprecated + public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + return new TangemBridgeProcessor(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + public static List getPendingRefundProcessedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PENDINGREFUNDPROCESSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PendingRefundProcessedEventResponse typedResponse = new PendingRefundProcessedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static PendingRefundProcessedEventResponse getPendingRefundProcessedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PENDINGREFUNDPROCESSED_EVENT, log); + PendingRefundProcessedEventResponse typedResponse = new PendingRefundProcessedEventResponse(); typedResponse.log = log; - typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); return typedResponse; } + public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + return new TangemBridgeProcessor(contractAddress, web3j, credentials, contractGasProvider); + } + + public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return new TangemBridgeProcessor(contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static List getRefundAccountSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(REFUNDACCOUNTSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + RefundAccountSetEventResponse typedResponse = new RefundAccountSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.refundAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static RemoteCall deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), + new Address(160, refundAccount_), + new Address(160, paymentAccountFactory_), + new Address(160, paymentToken_), + new Uint256(securityDelay_))); + return deployRemoteCall(TangemBridgeProcessor.class, web3j, credentials, contractGasProvider, BINARY, encodedConstructor); + } + + public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), + new Address(160, refundAccount_), + new Address(160, paymentAccountFactory_), + new Address(160, paymentToken_), + new Uint256(securityDelay_))); + return deployRemoteCall(TangemBridgeProcessor.class, web3j, transactionManager, contractGasProvider, BINARY, encodedConstructor); + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), + new Address(160, refundAccount_), + new Address(160, paymentAccountFactory_), + new Address(160, paymentToken_), + new Uint256(securityDelay_))); + return deployRemoteCall(TangemBridgeProcessor.class, web3j, credentials, gasPrice, gasLimit, BINARY, encodedConstructor); + } + + public static List getRefundProcessedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(REFUNDPROCESSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + RefundProcessedEventResponse typedResponse = new RefundProcessedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), + new Address(160, refundAccount_), + new Address(160, paymentAccountFactory_), + new Address(160, paymentToken_), + new Uint256(securityDelay_))); + return deployRemoteCall(TangemBridgeProcessor.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, encodedConstructor); + } + public Flowable authorizationChangeProcessedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getAuthorizationChangeProcessedEventFromLog(log)); } @@ -720,6 +642,20 @@ class TangemBridgeProcessor extends Contract { return authorizationChangeProcessedEventFlowable(filter); } + public static List getRoleAdminChangedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(ROLEADMINCHANGED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + RoleAdminChangedEventResponse typedResponse = new RoleAdminChangedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.role = (byte[]) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.previousAdminRole = (byte[]) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.newAdminRole = (byte[]) eventValues.getIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable authorizationProcessedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getAuthorizationProcessedEventFromLog(log)); } @@ -734,6 +670,20 @@ class TangemBridgeProcessor extends Contract { return web3j.ethLogFlowable(filter).map(log -> getDebtWriteOffProcessedEventFromLog(log)); } + public static List getRoleGrantedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(ROLEGRANTED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + RoleGrantedEventResponse typedResponse = new RoleGrantedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.role = (byte[]) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.account = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.sender = (String) eventValues.getIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable debtWriteOffProcessedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(DEBTWRITEOFFPROCESSED_EVENT)); @@ -750,10 +700,48 @@ class TangemBridgeProcessor extends Contract { return fixedFeeSetEventFlowable(filter); } + public static List getRoleRevokedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(ROLEREVOKED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + RoleRevokedEventResponse typedResponse = new RoleRevokedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.role = (byte[]) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.account = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.sender = (String) eventValues.getIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public Flowable pausedEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getPausedEventFromLog(log)); + } + + public Flowable pausedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(PAUSED_EVENT)); + return pausedEventFlowable(filter); + } + public Flowable paymentAccountSetEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getPaymentAccountSetEventFromLog(log)); } + public static List getSaveRefundProcessedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(SAVEREFUNDPROCESSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + SaveRefundProcessedEventResponse typedResponse = new SaveRefundProcessedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable paymentAccountSetEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(PAYMENTACCOUNTSET_EVENT)); @@ -770,6 +758,18 @@ class TangemBridgeProcessor extends Contract { return paymentReceiverSetEventFlowable(filter); } + public static List getSecurityDelaySetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(SECURITYDELAYSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + SecurityDelaySetEventResponse typedResponse = new SecurityDelaySetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.securityDelay = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable pendingRefundProcessedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getPendingRefundProcessedEventFromLog(log)); } @@ -784,6 +784,18 @@ class TangemBridgeProcessor extends Contract { return web3j.ethLogFlowable(filter).map(log -> getRefundAccountSetEventFromLog(log)); } + public static List getSettlementPeriodSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(SETTLEMENTPERIODSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + SettlementPeriodSetEventResponse typedResponse = new SettlementPeriodSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.settlementPeriod = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable refundAccountSetEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(REFUNDACCOUNTSET_EVENT)); @@ -800,6 +812,21 @@ class TangemBridgeProcessor extends Contract { return refundProcessedEventFlowable(filter); } + public static List getSettlementProcessedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(SETTLEMENTPROCESSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + SettlementProcessedEventResponse typedResponse = new SettlementProcessedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.fee = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable roleAdminChangedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getRoleAdminChangedEventFromLog(log)); } @@ -834,22 +861,25 @@ class TangemBridgeProcessor extends Contract { return web3j.ethLogFlowable(filter).map(log -> getSaveRefundProcessedEventFromLog(log)); } + public static List getVerifiedBalanceIncreasedForEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCEINCREASEDFOR_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + VerifiedBalanceIncreasedForEventResponse typedResponse = new VerifiedBalanceIncreasedForEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.increase = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable saveRefundProcessedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(SAVEREFUNDPROCESSED_EVENT)); return saveRefundProcessedEventFlowable(filter); } - public Flowable verifiedBalanceSetForEventFlowable(EthFilter filter) { - return web3j.ethLogFlowable(filter).map(log -> getVerifiedBalanceSetForEventFromLog(log)); - } - - public Flowable verifiedBalanceSetForEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { - EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); - filter.addSingleTopic(EventEncoder.encode(VERIFIEDBALANCESETFOR_EVENT)); - return verifiedBalanceSetForEventFlowable(filter); - } - public Flowable securityDelaySetEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getSecurityDelaySetEventFromLog(log)); } @@ -860,6 +890,38 @@ class TangemBridgeProcessor extends Contract { return securityDelaySetEventFlowable(filter); } + public static List getVerifiedBalanceSetForEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCESETFOR_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + VerifiedBalanceSetForEventResponse typedResponse = new VerifiedBalanceSetForEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static VerifiedBalanceSetForEventResponse getVerifiedBalanceSetForEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(VERIFIEDBALANCESETFOR_EVENT, log); + VerifiedBalanceSetForEventResponse typedResponse = new VerifiedBalanceSetForEventResponse(); + typedResponse.log = log; + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + return typedResponse; + } + + public Flowable verifiedBalanceSetForEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getVerifiedBalanceSetForEventFromLog(log)); + } + + public Flowable verifiedBalanceSetForEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(VERIFIEDBALANCESETFOR_EVENT)); + return verifiedBalanceSetForEventFlowable(filter); + } + public Flowable settlementPeriodSetEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getSettlementPeriodSetEventFromLog(log)); } @@ -880,6 +942,16 @@ class TangemBridgeProcessor extends Contract { return settlementProcessedEventFlowable(filter); } + public Flowable unpausedEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getUnpausedEventFromLog(log)); + } + + public Flowable unpausedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(UNPAUSED_EVENT)); + return unpausedEventFlowable(filter); + } + public Flowable verifiedBalanceIncreasedForEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getVerifiedBalanceIncreasedForEventFromLog(log)); } @@ -922,8 +994,24 @@ class TangemBridgeProcessor extends Contract { return executeRemoteCallSingleValueReturn(function, byte[].class); } - public RemoteFunctionCall PAYMENT_ACCOUNT_SETTER() { - final Function function = new Function(FUNC_PAYMENT_ACCOUNT_SETTER, + public RemoteFunctionCall FIXED_FEE_SETTER_ROLE() { + final Function function = new Function(FUNC_FIXED_FEE_SETTER_ROLE, + List.of(), + List.of(new TypeReference() { + })); + return executeRemoteCallSingleValueReturn(function, byte[].class); + } + + public RemoteFunctionCall PAUSER_ROLE() { + final Function function = new Function(FUNC_PAUSER_ROLE, + List.of(), + List.of(new TypeReference() { + })); + return executeRemoteCallSingleValueReturn(function, byte[].class); + } + + public RemoteFunctionCall PAYMENT_ACCOUNT_SETTER_ROLE() { + final Function function = new Function(FUNC_PAYMENT_ACCOUNT_SETTER_ROLE, List.of(), List.of(new TypeReference() { })); @@ -1022,6 +1110,22 @@ class TangemBridgeProcessor extends Contract { return executeRemoteCallTransaction(function); } + public RemoteFunctionCall pause() { + final Function function = new Function( + FUNC_PAUSE, + List.of(), + Collections.emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall paused() { + final Function function = new Function(FUNC_PAUSED, + List.of(), + List.of(new TypeReference() { + })); + return executeRemoteCallSingleValueReturn(function, Boolean.class); + } + public RemoteFunctionCall paymentAccountFactory() { final Function function = new Function(FUNC_PAYMENTACCOUNTFACTORY, List.of(), @@ -1107,12 +1211,11 @@ class TangemBridgeProcessor extends Contract { return executeRemoteCallTransaction(function); } - public RemoteFunctionCall processSettlement(String card, BigInteger transactionId, BigInteger settlementId, BigInteger amount) { + public RemoteFunctionCall processSettlement(String card, BigInteger transactionId, BigInteger amount) { final Function function = new Function( FUNC_PROCESSSETTLEMENT, Arrays.asList(new Address(160, card), new Uint256(transactionId), - new Uint256(settlementId), new Uint256(amount)), Collections.emptyList()); return executeRemoteCallTransaction(function); @@ -1236,6 +1339,14 @@ class TangemBridgeProcessor extends Contract { return executeRemoteCallSingleValueReturn(function, Boolean.class); } + public RemoteFunctionCall unpause() { + final Function function = new Function( + FUNC_UNPAUSE, + List.of(), + Collections.emptyList()); + return executeRemoteCallTransaction(function); + } + public RemoteFunctionCall writeOffDebt(String card, BigInteger amount) { final Function function = new Function( FUNC_WRITEOFFDEBT, @@ -1275,6 +1386,10 @@ class TangemBridgeProcessor extends Contract { public BigInteger fixedFee; } + public static class PausedEventResponse extends BaseEventResponse { + public String account; + } + public static class PaymentAccountSetEventResponse extends BaseEventResponse { public String card; @@ -1350,13 +1465,15 @@ class TangemBridgeProcessor extends Contract { public BigInteger transactionId; - public BigInteger settlementId; - public BigInteger amount; public BigInteger fee; } + public static class UnpausedEventResponse extends BaseEventResponse { + public String account; + } + public static class VerifiedBalanceIncreasedForEventResponse extends BaseEventResponse { public String paymentAccount; diff --git a/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccount.java b/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccount.java index 943509e28e..834d85f3c2 100644 --- a/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccount.java +++ b/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccount.java @@ -15,6 +15,7 @@ import org.web3j.protocol.core.methods.response.BaseEventResponse; import org.web3j.protocol.core.methods.response.Log; import org.web3j.protocol.core.methods.response.TransactionReceipt; import org.web3j.tuples.generated.Tuple3; +import org.web3j.tuples.generated.Tuple7; import org.web3j.tx.Contract; import org.web3j.tx.TransactionManager; import org.web3j.tx.gas.ContractGasProvider; @@ -32,14 +33,14 @@ import io.reactivex.Flowable; *

Auto generated code. *

Do not modify! *

Please use the web3j command line tools, - * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the + * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the * codegen module to update. * - *

Generated with web3j version 1.5.0. + *

Generated with web3j version 1.5.2. */ @SuppressWarnings("rawtypes") class TangemPaymentAccount extends Contract { - public static final String BINARY = "60c06040523060a05234801562000014575f80fd5b5060405162004c6938038062004c69833981016040819052620000379162000109565b6001600160a01b0381166080526200004e62000055565b5062000138565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000900460ff1615620000a65760405163f92ee8a960e01b815260040160405180910390fd5b80546001600160401b0390811614620001065780546001600160401b0319166001600160401b0390811782556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50565b5f602082840312156200011a575f80fd5b81516001600160a01b038116811462000131575f80fd5b9392505050565b60805160a051614af3620001765f395f8181612b9501528181612bbe0152612e1b01525f8181610439015281816104bb01526125500152614af35ff3fe60806040526004361061027f575f3560e01c80638c3b2a9b11610155578063ce1b1d43116100be578063e37b815411610078578063e37b8154146108c2578063e3dbffd51461092e578063e4d7ddb91461094d578063e6e268f41461096c578063f89db45714610980578063f92ad2191461099f575f80fd5b8063ce1b1d4314610831578063d47ae89c14610850578063dbed8faa14610864578063dcab7e5014610883578063df7727b4146102aa578063e1a8eafd146108a3575f80fd5b8063b4d02d0d1161010f578063b4d02d0d14610776578063b9603bdf14610795578063bb542ef0146107b4578063be69191d146107d3578063c45a0155146107e7578063c81656c814610806575f80fd5b80638c3b2a9b146106125780638cf351e7146106315780639335dcb714610650578063a075b7c71461066f578063a789457d1461068e578063ad3cb1cc14610739575f80fd5b8063456575e5116101f75780636b4943a7116101b15780636b4943a7146104985780637da0a877146104ad578063806679e4146104df578063860aefcf146104fe57806386f15d09146105df5780638bbe11af146105f3575f80fd5b8063456575e5146103c357806346236eb9146103e25780634f1ef286146103f657806352d1902d14610409578063572b6c051461041d5780636b2a383014610479575f80fd5b806322611280116102485780632261128014610308578063229865d11461031c5780633013ce291461033b57806334284dfb1461037157806335ba9af8146103905780633c1a5012146103a4575f80fd5b806274f3561461028357806301e948ff146102aa5780630f1071be146102be578063139d657c146102d257806319cdeb6f146102f3575b5f80fd5b34801561028e575f80fd5b506102976109be565b6040519081526020015b60405180910390f35b3480156102b5575f80fd5b506102976109d6565b3480156102c9575f80fd5b506102976109e5565b3480156102dd575f80fd5b506102f16102ec3660046140bc565b610a66565b005b3480156102fe575f80fd5b5061029760055481565b348015610313575f80fd5b506102f1610d5b565b348015610327575f80fd5b506102f16103363660046140e5565b610de8565b348015610346575f80fd5b505f54610359906001600160a01b031681565b6040516001600160a01b0390911681526020016102a1565b34801561037c575f80fd5b506102f161038b366004614136565b610e7f565b34801561039b575f80fd5b50610297610f45565b3480156103af575f80fd5b506102f16103be3660046141a1565b610fd4565b3480156103ce575f80fd5b506102f16103dd3660046141bc565b611060565b3480156103ed575f80fd5b50601b54610297565b6102f16104043660046141f0565b611100565b348015610414575f80fd5b5061029761111f565b348015610428575f80fd5b506104696104373660046141a1565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0390811691161490565b60405190151581526020016102a1565b348015610484575f80fd5b506102f16104933660046140e5565b61113a565b3480156104a3575f80fd5b5061029760065481565b3480156104b8575f80fd5b507f0000000000000000000000000000000000000000000000000000000000000000610359565b3480156104ea575f80fd5b506102f16104f93660046142ae565b611287565b348015610509575f80fd5b506040805160a08082018352600c54825282518084018452600d548152600e546020828101919091528084019190915283518085018552600f5481526010548183015283850152835180820185526011548152606080850191909152601254608080860191909152855193840186526013548452855180870187526014548152601554818501528484015285518087018752601654815260175481850152848701528551928301909552601854825282015260195492810192909252601a546105d0929083565b6040516102a19392919061432c565b3480156105ea575f80fd5b5061046961132a565b3480156105fe575f80fd5b506102f161060d3660046140e5565b611351565b34801561061d575f80fd5b506102f161062c366004614357565b611468565b34801561063c575f80fd5b506102f161064b366004614388565b611583565b34801561065b575f80fd5b50600354610359906001600160a01b031681565b34801561067a575f80fd5b506102f16106893660046143ca565b611710565b348015610699575f80fd5b506040805180820182526007546001600160a01b039081168252825180840184526008546001600160801b0319608082811b8216845261ffff600160801b938490048116602086810191909152808801959095528751808901895260095490961686528751808901909852600a549182901b9092168752919091041684820152810192909252600b5461072a929083565b6040516102a19392919061442a565b348015610744575f80fd5b50610769604051806040016040528060058152602001640352e302e360dc1b81525081565b6040516102a19190614475565b348015610781575f80fd5b506102f16107903660046140e5565b611801565b3480156107a0575f80fd5b506102f16107af3660046140e5565b611901565b3480156107bf575f80fd5b506102f16107ce3660046141a1565b611a37565b3480156107de575f80fd5b50610469611b08565b3480156107f2575f80fd5b50600154610359906001600160a01b031681565b348015610811575f80fd5b506102976108203660046140e5565b5f908152601c602052604090205490565b34801561083c575f80fd5b50600254610359906001600160a01b031681565b34801561085b575f80fd5b50610297611b2d565b34801561086f575f80fd5b506102f161087e3660046140e5565b611bad565b34801561088e575f80fd5b505f8051602061479083398151915254610297565b3480156108ae575f80fd5b506102f16108bd3660046141a1565b611c0c565b3480156108cd575f80fd5b506109096108dc3660046140e5565b60046020525f9081526040902080546001909101546001600160801b03811690600160801b900460ff1683565b604080519384526001600160801b0390921660208401521515908201526060016102a1565b348015610939575f80fd5b506102f16109483660046140e5565b611da8565b348015610958575f80fd5b506102f16109673660046141bc565b611f82565b348015610977575f80fd5b5061029761212a565b34801561098b575f80fd5b506102f161099a3660046140e5565b61213f565b3480156109aa575f80fd5b506102f16109b93660046144a7565b61230d565b5f805f805160206147708339815191525b5492915050565b5f6109e05f61247c565b905090565b5f6109e060025f9054906101000a90046001600160a01b03166001600160a01b0316630f1071be6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a39573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610a5d91906144e7565b6283d600612534565b6002546001600160a01b0316610a7a61254d565b6001600160a01b0316146040518060600160405280603381526020016145b96033913990610ac45760405162461bcd60e51b8152600401610abb9190614475565b60405180910390fd5b50610ace8161259e565b5f8381526004602052604081205490818311610b00575081808203828214610afa57610afa86826125f0565b50610b7f565b50805f610b0d600c612634565b80549091505f81851015610b4157610b30858303610b2b855f61264e565b612534565b9050610b3e81610b2b6109d6565b90505b8015610b67575f610b5482878903612534565b948501949050610b6584825f6126b9565b505b838603868514610b7a57610b7a816126e0565b505050505b816006541015604051806060016040528060298152602001614a186029913990610bbc5760405162461bcd60e51b8152600401610abb9190614475565b506006805483900390555f85815260046020818152604080842093845560019390930180546001600160881b0319169055600254835163659bf9d960e11b81529351610c5d946001600160a01b039092169363cb37f3b29383820193909291908290030181865afa158015610c33573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610c5791906144fe565b8261273f565b6040805184815260208101839052859187917f3e565e38537fb4064a62ad5c266c37a9798b673d92066b5bbbd2269c70078fd1910160405180910390a35f546040516370a0823160e01b81523060048201527f273f30c859762d267fd40559825ed2fa0f467908c92843dfd0184b9d577dc082916001600160a01b0316906370a0823190602401602060405180830381865afa158015610cff573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610d2391906144e7565b600654610d2e6109be565b601b5460408051948552602085019390935291830152606082015260800160405180910390a15050505050565b6003546001600160a01b0316610d6f61254d565b6001600160a01b03161480610da65750610d89600761284a565b546001600160a01b0316610d9b61254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f913990610ddd5760405162461bcd60e51b8152600401610abb9190614475565b50610de6612864565b565b6003546001600160a01b0316610dfc61254d565b6001600160a01b03161480610e335750610e16600761284a565b546001600160a01b0316610e2861254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f913990610e6a5760405162461bcd60e51b8152600401610abb9190614475565b50610e7c81610e77611b2d565b6128b2565b50565b6002546001600160a01b0316610e9361254d565b6001600160a01b0316146040518060600160405280603381526020016145b96033913990610ed45760405162461bcd60e51b8152600401610abb9190614475565b506001610ee2848484612963565b610eee868684846129bf565b604080518681526001600160801b03198616602082015261ffff851681830152905187917faab1db26b8ce0ed14d67fd3d8eab92617f4caf4134148122ad6f2a4e20068df7919081900360600190a2505050505050565b5f80546040516370a0823160e01b815230600482015282916001600160a01b0316906370a0823190602401602060405180830381865afa158015610f8b573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610faf91906144e7565b9050600654811015610fc1575f610fce565b600654610fce908261452d565b91505090565b6003546001600160a01b0316610fe861254d565b6001600160a01b0316148061101f5750611002600761284a565b546001600160a01b031661101461254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f9139906110565760405162461bcd60e51b8152600401610abb9190614475565b50610e7c81612a24565b6002546001600160a01b031661107461254d565b6001600160a01b0316146040518060600160405280603381526020016145b960339139906110b55760405162461bcd60e51b8152600401610abb9190614475565b506110c2601b8383612b0f565b817fea92e836c1b642a333bb58b44d5f13c5a066c5b0f35b8d6da5032af605c9e031826040516110f491815260200190565b60405180910390a25050565b611108612b8a565b61111182612c2e565b61111b8282612d54565b5050565b5f611128612e10565b505f8051602061484383398151915290565b6002546001600160a01b031661114e61254d565b6001600160a01b0316146040518060600160405280603381526020016145b9603391399061118f5760405162461bcd60e51b8152600401610abb9190614475565b505f8160055461119f9190614540565b5f546040516370a0823160e01b81523060048201529192506001600160a01b0316906370a0823190602401602060405180830381865afa1580156111e5573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061120991906144e7565b81111560405180608001604052806048815260200161468d60489139906112435760405162461bcd60e51b8152600401610abb9190614475565b50600581905560408051838152602081018390527fd6004694c2f543b69302481de26731f63c1d41062795b5366d8ca20fd3e3d6d391015b60405180910390a15050565b6002546001600160a01b031661129b61254d565b6001600160a01b0316146040518060600160405280603381526020016145b960339139906112dc5760405162461bcd60e51b8152600401610abb9190614475565b505f6112ea848484846129bf565b837f5e1266400272ee17509f7d7edb190c0fa83572f90677ee6a3bbfac22bf1f679d8460405161131c91815260200190565b60405180910390a250505050565b5f80516020614790833981519152545f905f80516020614730833981519152901515610fce565b5f818152600460205260409020600181015490546001600160801b03909116906113796109e5565b6113839083614540565b42116040518060600160405280602b81526020016145ec602b9139906113bc5760405162461bcd60e51b8152600401610abb9190614475565b50806006541015604051806060016040528060298152602001614a1860299139906113fa5760405162461bcd60e51b8152600401610abb9190614475565b506006805482900390555f8381526004602052604080822091825560019190910180546001600160881b03191690555183907f056bed1bad5d90aa1df671c8d28b1bbb82ff67fd545f804e63cd261140b11f7b9061145b9084815260200190565b60405180910390a2505050565b6003546001600160a01b031661147c61254d565b6001600160a01b031614806114b35750611496600761284a565b546001600160a01b03166114a861254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f9139906114ea5760405162461bcd60e51b8152600401610abb9190614475565b506040805180820182526009546001600160a01b0316815281518083019092526001600160801b03198416825261ffff831660208381019190915281019190915261154090611537611b2d565b60079190612e59565b604080516001600160801b03198416815261ffff831660208201527fc50d2cc66f62a75104edeabfbcedb2a37ebd1cb8ce0b873ec3afa504e8bb1480910161127b565b6003546001600160a01b031661159761254d565b6001600160a01b031614806115ce57506115b1600761284a565b546001600160a01b03166115c361254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f9139906116055760405162461bcd60e51b8152600401610abb9190614475565b50600b541561165e576116596040518060400160405280856001600160a01b031681526020016040518060400160405280866001600160801b03191681526020018561ffff16815250815250611537611b2d565b6116b1565b6116b16040518060400160405280856001600160a01b031681526020016040518060400160405280866001600160801b03191681526020018561ffff16815250815250600761300090919063ffffffff16565b604080516001600160a01b03851681526001600160801b03198416602082015261ffff8316918101919091527fae81d31085749debb22edb9a09f263e50cf6047cfc57ec741c07ce62b1121b9a906060015b60405180910390a1505050565b6003546001600160a01b031661172461254d565b6001600160a01b0316148061175b575061173e600761284a565b546001600160a01b031661175061254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f9139906117925760405162461bcd60e51b8152600401610abb9190614475565b506117b36117a2858585856130a1565b6117aa611b2d565b600c9190613102565b6040805185815260208101859052908101839052606081018290527ffc3ccf3ca5a38fad8786ed39386de19cb574b06205422992c583e4753aa6268f9060800160405180910390a150505050565b6003546001600160a01b031661181561254d565b6001600160a01b0316148061184c575061182f600761284a565b546001600160a01b031661184161254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f9139906118835760405162461bcd60e51b8152600401610abb9190614475565b50610e7c8160025f9054906101000a90046001600160a01b03166001600160a01b031663cb37f3b26040518163ffffffff1660e01b8152600401602060405180830381865afa1580156118d8573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906118fc91906144fe565b6131f0565b6002546001600160a01b031661191561254d565b6001600160a01b0316146040518060600160405280603381526020016145b960339139906119565760405162461bcd60e51b8152600401610abb9190614475565b505f546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa15801561199c573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906119c091906144e7565b81111560405180608001604052806048815260200161468d60489139906119fa5760405162461bcd60e51b8152600401610abb9190614475565b5060058190556040518181527f321b800465e47508430424e6c089344256e17f9a5b0b43c751dffbdfd3930263906020015b60405180910390a150565b6003546001600160a01b0316611a4b61254d565b6001600160a01b03161480611a825750611a65600761284a565b546001600160a01b0316611a7761254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f913990611ab95760405162461bcd60e51b8152600401610abb9190614475565b50600380546001600160a01b0319166001600160a01b0383169081179091556040519081527f0435c4db9db56b8cdd5d58a213f47d6fed6c2b35c81bbd6baf87547848f4c88b90602001611a2c565b5f5f80516020614730833981519152610fce5f805160206147908339815191526133a4565b5f6109e060025f9054906101000a90046001600160a01b03166001600160a01b031663d47ae89c6040518163ffffffff1660e01b8152600401602060405180830381865afa158015611b81573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190611ba591906144e7565b610e10612534565b6002546001600160a01b0316611bc161254d565b6001600160a01b0316146040518060600160405280603381526020016145b96033913990611c025760405162461bcd60e51b8152600401610abb9190614475565b50610e7c8161340b565b6003546001600160a01b0316611c2061254d565b6001600160a01b03161480611c575750611c3a600761284a565b546001600160a01b0316611c4c61254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f913990611c8e5760405162461bcd60e51b8152600401610abb9190614475565b50600254604080516060810190915260238082526001600160a01b039092161591614641602083013990611cd55760405162461bcd60e51b8152600401610abb9190614475565b50600280546001600160a01b0319166001600160a01b03831690811790915560408051633013ce2960e01b81529051633013ce29916004808201926020929091908290030181865afa158015611d2d573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190611d5191906144fe565b5f80546001600160a01b0319166001600160a01b0392831690811790915560408051928416835260208301919091527fb84a8bf26086331c9421c7ce6650da3b0ef748bd335c3f00ada56f481e4321939101611a2c565b6002546001600160a01b0316611dbc61254d565b6001600160a01b0316146040518060600160405280603381526020016145b96033913990611dfd5760405162461bcd60e51b8152600401610abb9190614475565b505f80546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa158015611e44573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190611e6891906144e7565b905060025f9054906101000a90046001600160a01b03166001600160a01b0316632cc326416040518163ffffffff1660e01b81526004015f604051808303815f87803b158015611eb6575f80fd5b505af1158015611ec8573d5f803e3d5ffd5b50505f80546040516370a0823160e01b81523060048201529193508492506001600160a01b0316906370a0823190602401602060405180830381865afa158015611f14573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190611f3891906144e7565b611f42919061452d565b9050611f50601b84836134e9565b827f2c6a6ee02f179bb5f13a4b7fdc9ccd1dd37e524b48ff0957a931348cc9ad75c08260405161145b91815260200190565b6002546001600160a01b0316611f9661254d565b6001600160a01b0316146040518060600160405280603381526020016145b96033913990611fd75760405162461bcd60e51b8152600401610abb9190614475565b505f60045f8481526020019081526020015f205f015490505f811160405180606001604052806023815260200161470d60239139906120295760405162461bcd60e51b8152600401610abb9190614475565b508082106040518060600160405280603b8152602001614929603b9139906120645760405162461bcd60e51b8152600401610abb9190614475565b5081810361207284826125f0565b806006541015604051806060016040528060298152602001614a1860299139906120af5760405162461bcd60e51b8152600401610abb9190614475565b506006805482900390555f8390036120e7575f84815260046020526040812090815560010180546001600160881b03191690556120f8565b5f8481526004602052604090208390555b837f8274e5392b3ab6bba68ee0567f4ede55e361239b8d0122fa5d3b5dc36497b91d8460405161131c91815260200190565b5f805f805160206147308339815191526109cf565b6002546001600160a01b031661215361254d565b6001600160a01b0316146040518060600160405280603381526020016145b960339139906121945760405162461bcd60e51b8152600401610abb9190614475565b505f80546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa1580156121db573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906121ff91906144e7565b905060025f9054906101000a90046001600160a01b03166001600160a01b0316632cc326416040518163ffffffff1660e01b81526004015f604051808303815f87803b15801561224d575f80fd5b505af115801561225f573d5f803e3d5ffd5b50505f80546040516370a0823160e01b81523060048201529193508492506001600160a01b0316906370a0823190602401602060405180830381865afa1580156122ab573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906122cf91906144e7565b6122d9919061452d565b9050827f2c6a6ee02f179bb5f13a4b7fdc9ccd1dd37e524b48ff0957a931348cc9ad75c08260405161145b91815260200190565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff16159067ffffffffffffffff165f811580156123525750825b90505f8267ffffffffffffffff16600114801561236e5750303b155b90508115801561237c575080155b1561239a5760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff1916600117855583156123c457845460ff60401b1916600160401b1785555b6123cc6135ce565b6123d46135ce565b6123dc6135ce565b6123e461254d565b600180546001600160a01b03199081166001600160a01b039384161790915560038054909116918c1691909117905561242a6124228a8a8a8a6130a1565b600c906135d6565b831561247057845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050505050565b5f80546040516370a0823160e01b815230600482015282916124f4916001600160a01b03909116906370a0823190602401602060405180830381865afa1580156124c8573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906124ec91906144e7565b600554612534565b600654909150836125145761250761212a565b6125119082614540565b90505b80821015612522575f61252c565b61252c818361452d565b949350505050565b5f8183106125425781612544565b825b90505b92915050565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031633148015612587575060143610155b15612599575060131936013560601c90565b503390565b60408051808201909152601b81527f353130307c436f6d6d6f6e3a20616d6f756e74206973207a65726f000000000060208201528161111b5760405162461bcd60e51b8152600401610abb9190614475565b5f828152600460205260409020600101546001600160801b03811690600160801b900460ff1661262e838383612626600c612634565b929190613662565b50505050565b5f4282600e015411156126475781612547565b5060070190565b5f61265b836005016133a4565b1561268c57811561267157506001820154612547565b600183015460038401546126859190612534565b9050612547565b811561269e5761268583600101613690565b6126856126ad84600101613690565b610b2b85600301613690565b6126c2836136b0565b6126cc83836136e0565b806126db576126db83836136ed565b505050565b5f805160206147708339815191528054829082905f90612701908490614540565b909155505080546040805184815260208101929092527ff3a2c404b3362c4238317c3dad1020c2e9e984ba8df38a4a1ff18d57e92a242a910161127b565b5f80546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa158015612785573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906127a991906144e7565b905080600554146040518060600160405280603381526020016149b760339139906127e75760405162461bcd60e51b8152600401610abb9190614475565b5060408051808201909152601f81525f8051602061475083398151915260208201528282101561282a5760405162461bcd60e51b8152600401610abb9190614475565b506005805483900390555f546126db906001600160a01b031684846136fa565b5f428260040154111561285d5781612547565b5060020190565b5f5f805160206147308339815191528181555f8051602061479083398151915282905560405190917f8892a543ee6d561b73cc067b2cde5a76c251a4e0b978d92390f1301db5d125ab91a150565b6128ba610f45565b8211156040518060400160405280601f81526020015f80516020614750833981519152815250906128fe5760405162461bcd60e51b8152600401610abb9190614475565b505f805160206147308339815191528281556129275f805160206147908339815191528361374c565b60018101546040805185815260208101929092527ffa4870dc2e2f95d11863e7e765a98e12fe92c3d1d2a703b4cf983f9c4b1052e69101611703565b600b54421015806129715750805b6040518060600160405280603581526020016148cd60359139906129a85760405162461bcd60e51b8152600401610abb9190614475565b506126db83836129b8600761284a565b91906137af565b60045f8581526020019081526020015f205f01545f1460405180606001604052806029815260200161498e6029913990612a0c5760405162461bcd60e51b8152600401610abb9190614475565b50612a188383836137ca565b61262e8484848461380b565b5f805160206147308339815191528054612a3c610f45565b8111156040518060400160405280601f81526020015f8051602061475083398151915281525090612a805760405162461bcd60e51b8152600401610abb9190614475565b50612a89611b08565b6040518060600160405280603881526020016146d56038913990612ac05760405162461bcd60e51b8152600401610abb9190614475565b505f8083556001830155612ad483826139a6565b826001600160a01b03167fa578b4c05763eb039caef37b2d7b949c22914c3838384c9c73cefb78c4a8ed0a8260405161145b91815260200190565b826001015f8381526020019081526020015f20545f146040518060600160405280602a8152602001614617602a913990612b5c5760405162461bcd60e51b8152600401610abb9190614475565b505f8281526001840160205260408120829055835482918591612b80908490614540565b9091555050505050565b306001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161480612c1057507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316612c045f80516020614843833981519152546001600160a01b031690565b6001600160a01b031614155b15610de65760405163703e46dd60e11b815260040160405180910390fd5b6003546001600160a01b0316612c4261254d565b6001600160a01b03161480612c795750612c5c600761284a565b546001600160a01b0316612c6e61254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f913990612cb05760405162461bcd60e51b8152600401610abb9190614475565b506001546040516316da1fb760e11b81526001600160a01b03838116600483015290911690632db43f6e90602401602060405180830381865afa158015612cf9573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190612d1d9190614553565b604051806060016040528060298152602001614a95602991399061111b5760405162461bcd60e51b8152600401610abb9190614475565b816001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa925050508015612dae575060408051601f3d908101601f19168201909252612dab918101906144e7565b60015b612dd657604051634c9c8ce360e01b81526001600160a01b0383166004820152602401610abb565b5f805160206148438339815191528114612e0657604051632a87526960e21b815260048101829052602401610abb565b6126db83836139dd565b306001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610de65760405163703e46dd60e11b815260040160405180910390fd5b602082015151600384015460801b6001600160801b03199081169116141580612e9c575060208083015101516003840154600160801b900461ffff908116911614155b80612eb95750815160028401546001600160a01b03908116911614155b6040518060600160405280602781526020016149026027913990612ef05760405162461bcd60e51b8152600401610abb9190614475565b506004830154421015612f695781516002840180546001600160a01b039092166001600160a01b03199092169190911790556020808301518051600386018054929093015161ffff16600160801b026001600160901b031990921660809190911c17179055612f5f8142614540565b6004840155505050565b60028301805484546001600160a01b03199081166001600160a01b0380841691909117875560038701805460018901805461ffff600160801b808504821681026001600160901b03199384166001600160801b03871617179093558a51909516959096169490941790955560208088015180519101519092169092029290931660809390931c92909217179055612f5f8142614540565b60048201541515156040518060600160405280602f815260200161458a602f91399061303f5760405162461bcd60e51b8152600401610abb9190614475565b5080516002830180546001600160a01b039092166001600160a01b03199092169190911790556020908101518051600384018054929093015161ffff16600160801b026001600160901b031990921660809190911c1717905542600490910155565b6130a961404a565b506040805160a081018252948552805180820182529384525f6020858101829052808701959095528151808301835293845283850152848101929092528151928301909152600182526060830191909152608082015290565b600e830154421015613166578151600784015560208083015180516008860155810151600985015560408301518051600a8601550151600b840155606082015151600c8401556080820151600d84015561315c8142614540565b600e840155505050565b600783018054845560088401805460018601556009850180546002870155600a860180546003880155600b870180546004890155600c8801805460058a0155600d8901805460068b015588519096556020808901518051909655948501519093556040870151805190925592015190915560608401515190556080830151905561315c8142614540565b5f805160206147708339815191525f6132076109d6565b825460408051808201909152601b81527f353130307c436f6d6d6f6e3a20616d6f756e74206973207a65726f00000000006020820152919250908561325f5760405162461bcd60e51b8152600401610abb9190614475565b5060408051808201909152601881527f353430317c446562743a206e6f206465627420666f756e6400000000000000006020820152816132b25760405162461bcd60e51b8152600401610abb9190614475565b5060408051808201909152601d81527f353430327c446562743a206e6f2066756e647320617661696c61626c650000006020820152826133055760405162461bcd60e51b8152600401610abb9190614475565b50808511156040518060600160405280602581526020016147d560259139906133415760405162461bcd60e51b8152600401610abb9190614475565b505f61334d8387612534565b8454819003855590506133608582613a32565b83546040805183815260208101929092527f75aec94f6ad949cfd9728eb818639f1b0b882089b480409ab73245c06469eef4910160405180910390a1505050505050565b5f6133af8254151590565b6040518060400160405280601d81526020017f353534317c54696d6572733a2074696d6572206e6f7420616374697665000000815250906134035760405162461bcd60e51b8152600401610abb9190614475565b505054421190565b5f80516020614770833981519152805460408051808201909152601b81527f353130307c436f6d6d6f6e3a20616d6f756e74206973207a65726f000000000060208201528361346d5760405162461bcd60e51b8152600401610abb9190614475565b50808311156040518060600160405280602581526020016147d560259139906134a95760405162461bcd60e51b8152600401610abb9190614475565b5081548390038083556040805185815260208101929092527fb940258db5193d85cafbc591d0c29fad88d804ff8b9ab71c8f9545af2bb2c07c9101611703565b5f836001015f8481526020019081526020015f205490505f81116040518060600160405280602581526020016147b0602591399061353a5760405162461bcd60e51b8152600401610abb9190614475565b50808214604051806060016040528060258152602001614a4160259139906135755760405162461bcd60e51b8152600401610abb9190614475565b50835f01548111156040518060600160405280602a8152602001614964602a9139906135b45760405162461bcd60e51b8152600401610abb9190614475565b508354038355505f90815260019091016020526040812055565b610de6613a3c565b600e8201541515156040518060600160405280602f815260200161458a602f9139906136155760405162461bcd60e51b8152600401610abb9190614475565b508051600783015560208082015180516008850155810151600984015560408201518051600a8501550151600b830155606081015151600c83015560800151600d82015542600e90910155565b61366c8483613a85565b1561262e5761367e6001850184613ac0565b8061262e5761262e6003850184613ac0565b805460018201545f9190808210156136a8575f61252c565b900392915050565b6136bc816005016133a4565b15610e7c575f60028201555f60048201556006810154610e7c90600583019061374c565b61111b6001830182613b0f565b61111b6003830182613b0f565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b1790526126db908490613b2b565b60408051808201909152601781527f353534307c54696d6572733a207a65726f2064656c617900000000000000000060208201528161379e5760405162461bcd60e51b8152600401610abb9190614475565b506137a98142614540565b90915550565b82546126db9060018501906001600160a01b03168484613b8c565b601a54421015806137d85750815b156137f3576126db83826137ec600c612634565b9190613d12565b6137ff600c8483613d12565b6126db60138483613d12565b6138148361259e565b5f821561387d575f613826600161247c565b905084811061383757849150613877565b809150857f83067e3478aa8b638f7e34eeecb23875cc3671e615445e892cc22dcd4c8b8df382870360405161386e91815260200190565b60405180910390a25b506138ce565b6138856109d6565b8411156040518060400160405280601f81526020015f80516020614750833981519152815250906138c95760405162461bcd60e51b8152600401610abb9190614475565b508390505b60408051808201909152601f81527f353331337c4163636f756e743a206e6f2066756e647320746f20626c6f636b006020820152816139205760405162461bcd60e51b8152600401610abb9190614475565b50604080516060810182528281526001600160801b0342811660208084019182528615158486019081525f8b815260049092529481209351845590516001909301805494511515600160801b026001600160881b03199095169390921692909217929092179091556006805483929061399a908490614540565b90915550505050505050565b8060055410156139b6575f6139c4565b806005546139c4919061452d565b6005555f5461111b906001600160a01b031683836136fa565b6139e682613d73565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a2805115613a2a576126db8282613dd6565b61111b613e48565b61111b828261273f565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff16610de657604051631afcd79f60e31b815260040160405180910390fd5b5f60058301613a93816133a4565b15613aa1575f915050612547565b600684015481545f91613ab39161452d565b8410159250612547915050565b80826001015410156040518060600160405280602e81526020016149ea602e913990613aff5760405162461bcd60e51b8152600401610abb9190614475565b5060019091018054919091039055565b80826001015f828254613b229190614540565b90915550505050565b5f613b3f6001600160a01b03841683613e67565b905080515f14158015613b63575080806020019051810190613b619190614553565b155b156126db57604051635274afe760e01b81526001600160a01b0384166004820152602401610abb565b83546040805160608101909152602980825261ffff600160801b909304831692841683119190614664602083013990613bd85760405162461bcd60e51b8152600401610abb9190614475565b5082825b8261ffff168161ffff161015613c94576040516bffffffffffffffffffffffff19606088901b1660208201526001600160f01b031960f083901b1660348201526001600160801b03198316603682015260029060460160408051601f1981840301815290829052613c4c9161456e565b602060405180830381855afa158015613c67573d5f803e3d5ffd5b5050506040513d601f19601f82011682018060405250810190613c8a91906144e7565b9150600101613bdc565b5085546040805160608101909152602180825260809290921b6001600160801b03199081169084161491614822602083013990613ce45760405162461bcd60e51b8152600401610abb9190614475565b5050845461ffff909216600160801b026001600160901b031990921660809390931c92909217179092555050565b825f01548211156040518060600160405280603b8152602001614892603b913990613d505760405162461bcd60e51b8152600401610abb9190614475565b50613d5a836136b0565b613d648383613e74565b806126db576126db8383613ec3565b806001600160a01b03163b5f03613da857604051634c9c8ce360e01b81526001600160a01b0382166004820152602401610abb565b5f8051602061484383398151915280546001600160a01b0319166001600160a01b0392909216919091179055565b60605f80846001600160a01b031684604051613df2919061456e565b5f60405180830381855af49150503d805f8114613e2a576040519150601f19603f3d011682016040523d82523d5f602084013e613e2f565b606091505b5091509150613e3f858383613f15565b95945050505050565b3415610de65760405163b398979f60e01b815260040160405180910390fd5b606061254483835f613f74565b613e81600183018261400d565b6040518060600160405280602881526020016147fa6028913990613eb85760405162461bcd60e51b8152600401610abb9190614475565b5061111b82826136e0565b613ed0600383018261400d565b6040518060600160405280602f8152602001614863602f913990613f075760405162461bcd60e51b8152600401610abb9190614475565b5061111b6003830182613b0f565b606082613f2a57613f2582614021565b613f6d565b8151158015613f4157506001600160a01b0384163b155b15613f6a57604051639996b31560e01b81526001600160a01b0385166004820152602401610abb565b50805b9392505050565b606081471015613f995760405163cd78605960e01b8152306004820152602401610abb565b5f80856001600160a01b03168486604051613fb4919061456e565b5f6040518083038185875af1925050503d805f8114613fee576040519150601f19603f3d011682016040523d82523d5f602084013e613ff3565b606091505b5091509150614003868383613f15565b9695505050505050565b5f61401783613690565b9091111592915050565b8051156140315780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b6040518060a001604052805f815260200161407660405180604001604052805f81526020015f81525090565b815260200161409660405180604001604052805f81526020015f81525090565b81526020016140b060405180602001604052805f81525090565b81526020015f81525090565b5f805f606084860312156140ce575f80fd5b505081359360208301359350604090920135919050565b5f602082840312156140f5575f80fd5b5035919050565b80356001600160801b031981168114614113575f80fd5b919050565b803561ffff81168114614113575f80fd5b8015158114610e7c575f80fd5b5f805f805f60a0868803121561414a575f80fd5b8535945060208601359350614161604087016140fc565b925061416f60608701614118565b9150608086013561417f81614129565b809150509295509295909350565b6001600160a01b0381168114610e7c575f80fd5b5f602082840312156141b1575f80fd5b8135613f6d8161418d565b5f80604083850312156141cd575f80fd5b50508035926020909101359150565b634e487b7160e01b5f52604160045260245ffd5b5f8060408385031215614201575f80fd5b823561420c8161418d565b9150602083013567ffffffffffffffff80821115614228575f80fd5b818501915085601f83011261423b575f80fd5b81358181111561424d5761424d6141dc565b604051601f8201601f19908116603f01168101908382118183101715614275576142756141dc565b8160405282815288602084870101111561428d575f80fd5b826020860160208301375f6020848301015280955050505050509250929050565b5f805f606084860312156142c0575f80fd5b833592506020840135915060408401356142d981614129565b809150509250925092565b8051825260208082015180518285015290810151604084015250604081015180516060840152602081015160808401525060608101515160a08301526080015160c090910152565b6101e0810161433b82866142e4565b61434860e08301856142e4565b826101c0830152949350505050565b5f8060408385031215614368575f80fd5b614371836140fc565b915061437f60208401614118565b90509250929050565b5f805f6060848603121561439a575f80fd5b83356143a58161418d565b92506143b3602085016140fc565b91506143c160408501614118565b90509250925092565b5f805f80608085870312156143dd575f80fd5b5050823594602084013594506040840135936060013592509050565b80516001600160a01b0316825260209081015180516001600160801b03191682840152015161ffff16604090910152565b60e0810161443882866143f9565b61444560608301856143f9565b8260c0830152949350505050565b5f5b8381101561446d578181015183820152602001614455565b50505f910152565b602081525f8251806020840152614493816040850160208701614453565b601f01601f19169190910160400192915050565b5f805f805f60a086880312156144bb575f80fd5b85356144c68161418d565b97602087013597506040870135966060810135965060800135945092505050565b5f602082840312156144f7575f80fd5b5051919050565b5f6020828403121561450e575f80fd5b8151613f6d8161418d565b634e487b7160e01b5f52601160045260245ffd5b8181038181111561254757612547614519565b8082018082111561254757612547614519565b5f60208284031215614563575f80fd5b8151613f6d81614129565b5f825161457f818460208701614453565b919091019291505056fe353530307c44656c6179656453657474696e67733a2076616c756520616c726561647920696e697469616c697a6564353330327c4163636f756e743a206f6e6c792070726f636573736f722063616e2063616c6c20746869732066756e6374696f6e353334307c4163636f756e743a20736574746c656d656e7420706572696f64206973206e6f74206f766572353432307c526566756e64733a20726566756e64207265636f726420616c726561647920657869737473353336307c4163636f756e743a2070726f636573736f7220616c726561647920736574353531307c4f6e6554696d6550617373776f72643a20696e76616c6964204f545020636f756e746572353335307c4163636f756e743a20726573756c74696e672076657269666965642062616c616e6365206e6f742065717569616c20746f2062616c616e636520616e64206e6f742030353431307c44656c617965645769746864726177616c733a207769746864726177616c2074696d656c6f636b206e6f742065787072696564353332307c4163636f756e743a207472616e73616374696f6e206e6f7420666f756e6476d8021c4b979e99674b80f4e0cc10154af0e5f32df05eb51b29a464e2435500353130317c436f6d6d6f6e3a20696e73756666696369656e742066756e6473003486e95df892f06bd37fd38c44e2fee4c4efb6660a66e6eb20ed6d8a167eae0076d8021c4b979e99674b80f4e0cc10154af0e5f32df05eb51b29a464e2435501353432317c526566756e64733a20726566756e64207265636f7264206e6f7420666f756e64353430337c446562743a20616d6f756e7420697320686967686572207468616e2064656274353532317c4163636f756e744c696d6974733a207370656e64206c696d6974206578636565646564353531317c4f6e6554696d6550617373776f72643a20696e76616c6964204f5450360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc353532337c4163636f756e744c696d6974733a206e6f206f7470207370656e64206c696d6974206578636565646564353532307c4163636f756e744c696d6974733a20616d6f756e7420657863656564732073696e676c65207472616e73616374696f6e206c696d6974353331327c4163636f756e743a2063617264206f72204f5450207374617465206368616e676520697320696e2070726f6772657373353530317c44656c6179656453657474696e67733a2076616c756520616c726561647920736574353332317c4163636f756e743a206e657720616d6f756e7420697320657175616c206f7220686967686572207468616e20617574686f72697a6564353432337c526566756e64733a2070656e64696e6720616d6f756e74206578636565647320746f74616c353331307c4163636f756e743a207472616e73616374696f6e20494420616c72656164792075736564353333307c4163636f756e743a2076657269666965642062616c616e6365206e6f7420657175616c20746f2062616c616e6365353533317c5370656e644c696d6974733a20696e73756666696369656e74207370656e7420746f2063616e63656c353330347c4163636f756e743a20696e73756666696369656e7420626c6f636b656420616d6f756e74353432327c526566756e64733a20696e76616c696420616d6f756e74207265636569766564353330317c4163636f756e743a206f6e6c79206f776e65722063616e2063616c6c20746869732066756e6374696f6e353337307c4163636f756e743a20756e617574686f72697a656420696d706c656d656e746174696f6ea264697066735822122012a8bf0b550c0636d300948851d60c701346e82b5d29b544481d654d6f3ba1f164736f6c63430008160033"; + public static final String BINARY = "60c06040523060a0523480156200001557600080fd5b5060405162005aa938038062005aa983398101604081905262000038916200010a565b6001600160a01b0381166080526200004f62000056565b506200013c565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000900460ff1615620000a75760405163f92ee8a960e01b815260040160405180910390fd5b80546001600160401b0390811614620001075780546001600160401b0319166001600160401b0390811782556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50565b6000602082840312156200011d57600080fd5b81516001600160a01b03811681146200013557600080fd5b9392505050565b60805160a05161592b6200017e600039600081816132fe0152818161332701526135880152600081816104d301528181610578015261290d015261592b6000f3fe6080604052600436106102c85760003560e01c806386489ba911610175578063c470d703116100dc578063df7727b411610095578063e3dbffd51161006f578063e3dbffd514610a40578063e4d7ddb914610a60578063e6e268f414610a80578063f89db45714610a9557600080fd5b8063df7727b4146102f5578063e1a8eafd146109b2578063e37b8154146109d257600080fd5b8063c470d703146108da578063c81656c81461090e578063ce1b1d431461093b578063d47ae89c1461095b578063dbed8faa14610970578063dcab7e501461099057600080fd5b8063a789457d1161012e578063a789457d1461077b578063ad3cb1cc14610827578063b4d02d0d14610865578063b9603bdf14610885578063be69191d146108a5578063c45a0155146108ba57600080fd5b806386489ba9146106c657806386f15d09146106e65780638bbe11af146106fb5780638c3b2a9b1461071b5780638da5cb5b1461073b578063a075b7c71461075b57600080fd5b80633c1a5012116102345780636b2a3830116101ed5780637da0a877116101c75780637da0a87714610569578063806679e41461059c57806384b0196e146105bc578063860aefcf146105e457600080fd5b80636b2a3830146105135780636b4943a7146105335780637b1039991461054957600080fd5b80633c1a501214610439578063456575e51461045957806346236eb9146104795780634f1ef2861461048e57806352d1902d146104a1578063572b6c05146104b657600080fd5b806322611280116102865780632261128014610377578063229865d11461038c5780633013ce29146103ac578063309433ba146103e457806334284dfb1461040457806335ba9af81461042457600080fd5b806274f356146102cd57806301e948ff146102f55780630381b4dd1461030a5780630f1071be1461032c57806319cdeb6f146103415780631f852f4b14610357575b600080fd5b3480156102d957600080fd5b506102e2610ab5565b6040519081526020015b60405180910390f35b34801561030157600080fd5b506102e2610acf565b34801561031657600080fd5b5061032a610325366004614b0d565b610ae0565b005b34801561033857600080fd5b506102e2610dde565b34801561034d57600080fd5b506102e260065481565b34801561036357600080fd5b5061032a610372366004614b8d565b610e63565b34801561038357600080fd5b5061032a610fa7565b34801561039857600080fd5b5061032a6103a7366004614be2565b611034565b3480156103b857600080fd5b506000546103cc906001600160a01b031681565b6040516001600160a01b0390911681526020016102ec565b3480156103f057600080fd5b5061032a6103ff366004614c2a565b6110cb565b34801561041057600080fd5b5061032a61041f366004614cb0565b611337565b34801561043057600080fd5b506102e26113fd565b34801561044557600080fd5b5061032a610454366004614d0b565b611490565b34801561046557600080fd5b5061032a610474366004614b0d565b61151c565b34801561048557600080fd5b50601c546102e2565b61032a61049c366004614d3e565b6115bc565b3480156104ad57600080fd5b506102e26115db565b3480156104c257600080fd5b506105036104d1366004614d0b565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0390811691161490565b60405190151581526020016102ec565b34801561051f57600080fd5b5061032a61052e366004614be2565b6115f8565b34801561053f57600080fd5b506102e260075481565b34801561055557600080fd5b506002546103cc906001600160a01b031681565b34801561057557600080fd5b507f00000000000000000000000000000000000000000000000000000000000000006103cc565b3480156105a857600080fd5b5061032a6105b7366004614e02565b611749565b3480156105c857600080fd5b506105d16117ed565b6040516102ec9796959493929190614e8b565b3480156105f057600080fd5b506040805160a08082018352600d54825282518084018452600e548152600f546020828101919091528084019190915283518085018552601054815260115481830152838501528351808201855260125481526060808501919091526013546080808601919091528551938401865260145484528551808701875260155481526016548185015284840152855180870187526017548152601854818501528487015285519283019095526019548252820152601a5492810192909252601b546106b7929083565b6040516102ec93929190614f6c565b3480156106d257600080fd5b5061032a6106e1366004614f97565b611899565b3480156106f257600080fd5b50610503611a66565b34801561070757600080fd5b5061032a610716366004614be2565b611a90565b34801561072757600080fd5b5061032a610736366004614ff0565b611ba9565b34801561074757600080fd5b506004546103cc906001600160a01b031681565b34801561076757600080fd5b5061032a610776366004615023565b611cf8565b34801561078757600080fd5b506040805180820182526008546001600160a01b039081168252825180840184526009546001600160801b0319608082811b8216845261ffff600160801b9384900481166020868101919091528088019590955287518089018952600a5490961686528751808901909852600b549182901b9092168752919091041684820152810192909252600c54610818929083565b6040516102ec93929190615086565b34801561083357600080fd5b50610858604051806040016040528060058152602001640352e302e360dc1b81525081565b6040516102ec91906150af565b34801561087157600080fd5b5061032a610880366004614be2565b611de0565b34801561089157600080fd5b5061032a6108a0366004614be2565b611ee3565b3480156108b157600080fd5b5061050361201c565b3480156108c657600080fd5b506001546103cc906001600160a01b031681565b3480156108e657600080fd5b507f3fc394ec90f88dd7e18c46ffeef55f1b6719a2027f4ae9fbc4ab118271891200546102e2565b34801561091a57600080fd5b506102e2610929366004614be2565b6000908152601d602052604090205490565b34801561094757600080fd5b506003546103cc906001600160a01b031681565b34801561096757600080fd5b506102e2612044565b34801561097c57600080fd5b5061032a61098b366004614be2565b6120c8565b34801561099c57600080fd5b506000805160206154fc833981519152546102e2565b3480156109be57600080fd5b5061032a6109cd366004614d0b565b612127565b3480156109de57600080fd5b50610a1b6109ed366004614be2565b600560205260009081526040902080546001909101546001600160801b03811690600160801b900460ff1683565b604080519384526001600160801b0390921660208401521515908201526060016102ec565b348015610a4c57600080fd5b5061032a610a5b366004614be2565b6122c6565b348015610a6c57600080fd5b5061032a610a7b366004614b0d565b6124ac565b348015610a8c57600080fd5b506102e261265c565b348015610aa157600080fd5b5061032a610ab0366004614be2565b612673565b6000806000805160206154dc8339815191525b5492915050565b6000610adb600061284d565b905090565b6003546001600160a01b0316610af4612909565b6001600160a01b0316146040518060600160405280603381526020016153166033913990610b3e5760405162461bcd60e51b8152600401610b3591906150af565b60405180910390fd5b50610b488161295b565b60008281526005602052604081205490818311610b7b575081808203828214610b7557610b75858261299b565b50610bff565b50806000610b89600d6129e0565b8054909150600081851015610bbf57610bae858303610ba98560006129ff565b612a6b565b9050610bbc81610ba9610acf565b90505b8015610be7576000610bd382878903612a6b565b948501949050610be584826000612a83565b505b838603868514610bfa57610bfa81612aaa565b505050505b8160075410156040518060600160405280602981526020016158226029913990610c3c5760405162461bcd60e51b8152600401610b3591906150af565b50600780548390039055600084815260056020908152604080832092835560019290920180546001600160881b0319169055600354825163659bf9d960e11b81529251610cdf936001600160a01b039092169263cb37f3b292600480820193918290030181865afa158015610cb5573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610cd991906150c2565b82612b0b565b604080518481526020810183905285917fa06caae8c696e5deb33f685024044b74125cb932e11521aef47de748321657c4910160405180910390a26000546040516370a0823160e01b81523060048201527f273f30c859762d267fd40559825ed2fa0f467908c92843dfd0184b9d577dc082916001600160a01b0316906370a0823190602401602060405180830381865afa158015610d82573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610da691906150df565b600754610db1610ab5565b601c546040805194855260208501939093529183015260608201526080015b60405180910390a150505050565b6000610adb600360009054906101000a90046001600160a01b03166001600160a01b0316630f1071be6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610e36573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e5a91906150df565b6283d600612a6b565b6004546001600160a01b0316610e77612909565b6001600160a01b03161480610eae5750610e916008612c1b565b546001600160a01b0316610ea3612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f913990610ee55760405162461bcd60e51b8152600401610b3591906150af565b50610ef1838383612c36565b600480546001600160a01b038581166001600160a01b0319831681178455600254604051635cd2673760e01b815293831694840185905260248401919091521690635cd2673790604401600060405180830381600087803b158015610f5557600080fd5b505af1158015610f69573d6000803e3d6000fd5b50506040516001600160a01b03871681527f50146d0e3c60aa1d17a70635b05494f864e86144a2201275021014fbf08bafe292506020019050610dd0565b6004546001600160a01b0316610fbb612909565b6001600160a01b03161480610ff25750610fd56008612c1b565b546001600160a01b0316610fe7612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f9139906110295760405162461bcd60e51b8152600401610b3591906150af565b50611032612d2a565b565b6004546001600160a01b0316611048612909565b6001600160a01b0316148061107f57506110626008612c1b565b546001600160a01b0316611074612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f9139906110b65760405162461bcd60e51b8152600401610b3591906150af565b506110c8816110c3612044565b612d7b565b50565b6004546001600160a01b03166110df612909565b6001600160a01b0316148061111657506110f96008612c1b565b546001600160a01b031661110b612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f91399061114d5760405162461bcd60e51b8152600401610b3591906150af565b50611159858383612c36565b600c541561122857600a546040805180820182526001600160a01b03888116825282518084019093526001600160801b03198816835261ffff87166020848101919091528201929092529116906111bb906111b2612044565b60089190612e79565b600254604051630747fc3760e41b81526001600160a01b03838116600483015288811660248301529091169063747fc37090604401600060405180830381600087803b15801561120a57600080fd5b505af115801561121e573d6000803e3d6000fd5b50505050506112db565b61127b6040518060400160405280876001600160a01b031681526020016040518060400160405280886001600160801b03191681526020018761ffff16815250815250600861302090919063ffffffff16565b60025460405163346b888d60e11b81526001600160a01b038781166004830152909116906368d7111a90602401600060405180830381600087803b1580156112c257600080fd5b505af11580156112d6573d6000803e3d6000fd5b505050505b604080516001600160a01b03871681526001600160801b03198616602082015261ffff85168183015290517fae81d31085749debb22edb9a09f263e50cf6047cfc57ec741c07ce62b1121b9a9181900360600190a15050505050565b6003546001600160a01b031661134b612909565b6001600160a01b031614604051806060016040528060338152602001615316603391399061138c5760405162461bcd60e51b8152600401610b3591906150af565b50600161139a8484846130c1565b6113a68686848461311d565b604080518681526001600160801b03198616602082015261ffff851681830152905187917faab1db26b8ce0ed14d67fd3d8eab92617f4caf4134148122ad6f2a4e20068df7919081900360600190a2505050505050565b600080546040516370a0823160e01b815230600482015282916001600160a01b0316906370a0823190602401602060405180830381865afa158015611446573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061146a91906150df565b905060075481101561147d57600061148a565b60075461148a908261510e565b91505090565b6004546001600160a01b03166114a4612909565b6001600160a01b031614806114db57506114be6008612c1b565b546001600160a01b03166114d0612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f9139906115125760405162461bcd60e51b8152600401610b3591906150af565b506110c881613186565b6003546001600160a01b0316611530612909565b6001600160a01b03161460405180606001604052806033815260200161531660339139906115715760405162461bcd60e51b8152600401610b3591906150af565b5061157e601c8383613274565b817fea92e836c1b642a333bb58b44d5f13c5a066c5b0f35b8d6da5032af605c9e031826040516115b091815260200190565b60405180910390a25050565b6115c46132f3565b6115cd82613398565b6115d782826134c0565b5050565b60006115e561357d565b506000805160206155cf83398151915290565b6003546001600160a01b031661160c612909565b6001600160a01b031614604051806060016040528060338152602001615316603391399061164d5760405162461bcd60e51b8152600401610b3591906150af565b5060008160065461165e9190615121565b6000546040516370a0823160e01b81523060048201529192506001600160a01b0316906370a0823190602401602060405180830381865afa1580156116a7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906116cb91906150df565b8111156040518060600160405280603781526020016153c760379139906117055760405162461bcd60e51b8152600401610b3591906150af565b50600681905560408051838152602081018390527fd6004694c2f543b69302481de26731f63c1d41062795b5366d8ca20fd3e3d6d391015b60405180910390a15050565b6003546001600160a01b031661175d612909565b6001600160a01b031614604051806060016040528060338152602001615316603391399061179e5760405162461bcd60e51b8152600401610b3591906150af565b5060006117ad8484848461311d565b837f5e1266400272ee17509f7d7edb190c0fa83572f90677ee6a3bbfac22bf1f679d846040516117df91815260200190565b60405180910390a250505050565b60006060808280808381600080516020615541833981519152805490915015801561181a57506001810154155b61185e5760405162461bcd60e51b81526020600482015260156024820152741152540dcc4c8e88155b9a5b9a5d1a585b1a5e9959605a1b6044820152606401610b35565b6118666135c6565b61186e613689565b60408051600080825260208201909252600f60f81b9c939b5091995046985030975095509350915050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff16159067ffffffffffffffff166000811580156118df5750825b905060008267ffffffffffffffff1660011480156118fc5750303b155b90508115801561190a575080155b156119285760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561195257845460ff60401b1916600160401b1785555b61195a6136c8565b6119626136c8565b6119b16040518060400160405280601481526020017315185b99d95b54185e5b595b9d1058d8dbdd5b9d60621b815250604051806040016040528060018152602001603160f81b8152506136d0565b6119b96136c8565b6119c1612909565b600180546001600160a01b03199081166001600160a01b03938416179091556004805482168e841617905560028054909116918c16919091179055611a13611a0b8a8a8a8a6136e2565b600d90613780565b8315611a5957845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b5050505050505050505050565b6000805160206154fc8339815191525460009060008051602061549c83398151915290151561148a565b6000818152600560205260409020600181015490546001600160801b0390911690611ab9610dde565b611ac39083615121565b42116040518060600160405280602b8152602001615349602b913990611afc5760405162461bcd60e51b8152600401610b3591906150af565b508060075410156040518060600160405280602981526020016158226029913990611b3a5760405162461bcd60e51b8152600401610b3591906150af565b5060078054829003905560008381526005602052604080822091825560019190910180546001600160881b03191690555183907f056bed1bad5d90aa1df671c8d28b1bbb82ff67fd545f804e63cd261140b11f7b90611b9c9084815260200190565b60405180910390a2505050565b6004546001600160a01b0316611bbd612909565b6001600160a01b03161480611bf45750611bd76008612c1b565b546001600160a01b0316611be9612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f913990611c2b5760405162461bcd60e51b8152600401610b3591906150af565b50600c5415156040518060600160405280603181526020016157c66031913990611c685760405162461bcd60e51b8152600401610b3591906150af565b50604080518082018252600a546001600160a01b0316815281518083019092526001600160801b03198416825261ffff8316602083810191909152810191909152611cb5906111b2612044565b604080516001600160801b03198416815261ffff831660208201527fc50d2cc66f62a75104edeabfbcedb2a37ebd1cb8ce0b873ec3afa504e8bb1480910161173d565b6004546001600160a01b0316611d0c612909565b6001600160a01b03161480611d435750611d266008612c1b565b546001600160a01b0316611d38612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f913990611d7a5760405162461bcd60e51b8152600401610b3591906150af565b50611d9b611d8a858585856136e2565b611d92612044565b600d919061380c565b6040805185815260208101859052908101839052606081018290527ffc3ccf3ca5a38fad8786ed39386de19cb574b06205422992c583e4753aa6268f90608001610dd0565b6004546001600160a01b0316611df4612909565b6001600160a01b03161480611e2b5750611e0e6008612c1b565b546001600160a01b0316611e20612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f913990611e625760405162461bcd60e51b8152600401610b3591906150af565b506110c881600360009054906101000a90046001600160a01b03166001600160a01b031663cb37f3b26040518163ffffffff1660e01b8152600401602060405180830381865afa158015611eba573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ede91906150c2565b6138fa565b6003546001600160a01b0316611ef7612909565b6001600160a01b0316146040518060600160405280603381526020016153166033913990611f385760405162461bcd60e51b8152600401610b3591906150af565b506000546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa158015611f81573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611fa591906150df565b8111156040518060600160405280603781526020016153c76037913990611fdf5760405162461bcd60e51b8152600401610b3591906150af565b5060068190556040518181527f321b800465e47508430424e6c089344256e17f9a5b0b43c751dffbdfd3930263906020015b60405180910390a150565b600060008051602061549c83398151915261148a6000805160206154fc833981519152613a4c565b6000610adb600360009054906101000a90046001600160a01b03166001600160a01b031663d47ae89c6040518163ffffffff1660e01b8152600401602060405180830381865afa15801561209c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906120c091906150df565b610e10612a6b565b6003546001600160a01b03166120dc612909565b6001600160a01b031614604051806060016040528060338152602001615316603391399061211d5760405162461bcd60e51b8152600401610b3591906150af565b506110c881613ab4565b6004546001600160a01b031661213b612909565b6001600160a01b0316148061217257506121556008612c1b565b546001600160a01b0316612167612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f9139906121a95760405162461bcd60e51b8152600401610b3591906150af565b50600354604080516060810190915260238082526001600160a01b0390921615916153fe6020830139906121f05760405162461bcd60e51b8152600401610b3591906150af565b50600380546001600160a01b0319166001600160a01b03831690811790915560408051633013ce2960e01b81529051633013ce29916004808201926020929091908290030181865afa15801561224a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061226e91906150c2565b600080546001600160a01b0319166001600160a01b0392831690811790915560408051928416835260208301919091527fb84a8bf26086331c9421c7ce6650da3b0ef748bd335c3f00ada56f481e4321939101612011565b6003546001600160a01b03166122da612909565b6001600160a01b031614604051806060016040528060338152602001615316603391399061231b5760405162461bcd60e51b8152600401610b3591906150af565b50600080546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa158015612365573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061238991906150df565b9050600360009054906101000a90046001600160a01b03166001600160a01b0316632cc326416040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156123db57600080fd5b505af11580156123ef573d6000803e3d6000fd5b5050600080546040516370a0823160e01b81523060048201529193508492506001600160a01b0316906370a0823190602401602060405180830381865afa15801561243e573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061246291906150df565b61246c919061510e565b905061247a601c8483613b81565b827f2c6a6ee02f179bb5f13a4b7fdc9ccd1dd37e524b48ff0957a931348cc9ad75c082604051611b9c91815260200190565b6003546001600160a01b03166124c0612909565b6001600160a01b03161460405180606001604052806033815260200161531660339139906125015760405162461bcd60e51b8152600401610b3591906150af565b506000600560008481526020019081526020016000206000015490506000811160405180606001604052806023815260200161547960239139906125585760405162461bcd60e51b8152600401610b3591906150af565b508082106040518060600160405280603b81526020016156d7603b9139906125935760405162461bcd60e51b8152600401610b3591906150af565b508181036125a1848261299b565b80600754101560405180606001604052806029815260200161582260299139906125de5760405162461bcd60e51b8152600401610b3591906150af565b50600780548290039055600083900361261857600084815260056020526040812090815560010180546001600160881b031916905561262a565b60008481526005602052604090208390555b837f8274e5392b3ab6bba68ee0567f4ede55e361239b8d0122fa5d3b5dc36497b91d846040516117df91815260200190565b60008060008051602061549c833981519152610ac8565b6003546001600160a01b0316612687612909565b6001600160a01b03161460405180606001604052806033815260200161531660339139906126c85760405162461bcd60e51b8152600401610b3591906150af565b50600080546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa158015612712573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061273691906150df565b9050600360009054906101000a90046001600160a01b03166001600160a01b0316632cc326416040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561278857600080fd5b505af115801561279c573d6000803e3d6000fd5b5050600080546040516370a0823160e01b81523060048201529193508492506001600160a01b0316906370a0823190602401602060405180830381865afa1580156127eb573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061280f91906150df565b612819919061510e565b9050827f2c6a6ee02f179bb5f13a4b7fdc9ccd1dd37e524b48ff0957a931348cc9ad75c082604051611b9c91815260200190565b600080546040516370a0823160e01b815230600482015282916128c8916001600160a01b03909116906370a0823190602401602060405180830381865afa15801561289c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906128c091906150df565b600654612a6b565b600754909150836128e8576128db61265c565b6128e59082615121565b90505b808210156128f7576000612901565b612901818361510e565b949350505050565b60007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031633148015612944575060143610155b15612956575060131936013560601c90565b503390565b60408051808201909152601b81526000805160206154218339815191526020820152816115d75760405162461bcd60e51b8152600401610b3591906150af565b6000828152600560205260409020600101546001600160801b03811690600160801b900460ff166129da8383836129d2600d6129e0565b929190613c6c565b50505050565b60004282600e015411156129f457816129f9565b816007015b92915050565b6000612a0d83600501613a4c565b15612a3e578115612a23575060018201546129f9565b60018301546003840154612a379190612a6b565b90506129f9565b8115612a5057612a3783600101613c9a565b612a37612a5f84600101613c9a565b610ba985600301613c9a565b6000818310612a7a5781612a7c565b825b9392505050565b612a8c83613cbc565b612a968383613cee565b80612aa557612aa58383613cfb565b505050565b6000805160206154dc833981519152805482908290600090612acd908490615121565b909155505080546040805184815260208101929092527ff3a2c404b3362c4238317c3dad1020c2e9e984ba8df38a4a1ff18d57e92a242a910161173d565b600080546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa158015612b54573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612b7891906150df565b905080600654146040518060600160405280603381526020016157656033913990612bb65760405162461bcd60e51b8152600401610b3591906150af565b5060408051808201909152601f81526000805160206154bc833981519152602082015282821015612bfa5760405162461bcd60e51b8152600401610b3591906150af565b50600680548390039055600054612aa5906001600160a01b03168484613d08565b60004282600401541115612c2f57816129f9565b5060020190565b60408051602081019091527f3fc394ec90f88dd7e18c46ffeef55f1b6719a2027f4ae9fbc4ab1182718912008054825290600090612c7c90612c7790613d5a565b613db2565b90506000612cc08286868080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250613ddf92505050565b9050856001600160a01b0316816001600160a01b0316146040518060600160405280602b81526020016157f7602b913990612d0e5760405162461bcd60e51b8152600401610b3591906150af565b508254836000612d1d83615134565b9190505550505050505050565b600060008051602061549c8339815191528181556000805160206154fc83398151915282905560405190917f8892a543ee6d561b73cc067b2cde5a76c251a4e0b978d92390f1301db5d125ab91a150565b60408051808201909152601b8152600080516020615421833981519152602082015282612dbb5760405162461bcd60e51b8152600401610b3591906150af565b50612dc46113fd565b8211156040518060400160405280601f81526020016000805160206154bc83398151915281525090612e095760405162461bcd60e51b8152600401610b3591906150af565b5060008051602061549c833981519152828155612e346000805160206154fc83398151915283613e09565b60018101546040805185815260208101929092527ffa4870dc2e2f95d11863e7e765a98e12fe92c3d1d2a703b4cf983f9c4b1052e691015b60405180910390a1505050565b602082015151600384015460801b6001600160801b03199081169116141580612ebc575060208083015101516003840154600160801b900461ffff908116911614155b80612ed95750815160028401546001600160a01b03908116911614155b60405180606001604052806027815260200161568e6027913990612f105760405162461bcd60e51b8152600401610b3591906150af565b506004830154421015612f895781516002840180546001600160a01b039092166001600160a01b03199092169190911790556020808301518051600386018054929093015161ffff16600160801b026001600160901b031990921660809190911c17179055612f7f8142615121565b6004840155505050565b60028301805484546001600160a01b03199081166001600160a01b0380841691909117875560038701805460018901805461ffff600160801b808504821681026001600160901b03199384166001600160801b03871617179093558a51909516959096169490941790955560208088015180519101519092169092029290931660809390931c92909217179055612f7f8142615121565b60048201541515156040518060600160405280602f81526020016152e7602f91399061305f5760405162461bcd60e51b8152600401610b3591906150af565b5080516002830180546001600160a01b039092166001600160a01b03199092169190911790556020908101518051600384018054929093015161ffff16600160801b026001600160901b031990921660809190911c1717905542600490910155565b600c54421015806130cf5750805b60405180606001604052806035815260200161565960359139906131065760405162461bcd60e51b8152600401610b3591906150af565b50612aa583836131166008612c1b565b9190613e6c565b600560008581526020019081526020016000206000015460001460405180606001604052806029815260200161573c602991399061316e5760405162461bcd60e51b8152600401610b3591906150af565b5061317a838383613e87565b6129da84848484613ec8565b60008051602061549c833981519152805461319f6113fd565b8111156040518060400160405280601f81526020016000805160206154bc833981519152815250906131e45760405162461bcd60e51b8152600401610b3591906150af565b506131ed61201c565b60405180606001604052806038815260200161544160389139906132245760405162461bcd60e51b8152600401610b3591906150af565b50600080835560018301556132398382614067565b826001600160a01b03167fa578b4c05763eb039caef37b2d7b949c22914c3838384c9c73cefb78c4a8ed0a82604051611b9c91815260200190565b826001016000838152602001908152602001600020546000146040518060600160405280602a8152602001615374602a9139906132c45760405162461bcd60e51b8152600401610b3591906150af565b50600082815260018401602052604081208290558354829185916132e9908490615121565b9091555050505050565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148061337a57507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031661336e6000805160206155cf833981519152546001600160a01b031690565b6001600160a01b031614155b156110325760405163703e46dd60e11b815260040160405180910390fd5b6004546001600160a01b03166133ac612909565b6001600160a01b031614806133e357506133c66008612c1b565b546001600160a01b03166133d8612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f91399061341a5760405162461bcd60e51b8152600401610b3591906150af565b506001546040516316da1fb760e11b81526001600160a01b03838116600483015290911690632db43f6e90602401602060405180830381865afa158015613465573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190613489919061514d565b6040518060600160405280602981526020016158cd60299139906115d75760405162461bcd60e51b8152600401610b3591906150af565b816001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa92505050801561351a575060408051601f3d908101601f19168201909252613517918101906150df565b60015b61354257604051634c9c8ce360e01b81526001600160a01b0383166004820152602401610b35565b6000805160206155cf833981519152811461357357604051632a87526960e21b815260048101829052602401610b35565b612aa583836140a0565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146110325760405163703e46dd60e11b815260040160405180910390fd5b7fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d1028054606091600080516020615541833981519152916136059061516a565b80601f01602080910402602001604051908101604052809291908181526020018280546136319061516a565b801561367e5780601f106136535761010080835404028352916020019161367e565b820191906000526020600020905b81548152906001019060200180831161366157829003601f168201915b505050505091505090565b7fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d1038054606091600080516020615541833981519152916136059061516a565b6110326140f6565b6136d86140f6565b6115d7828261413f565b6136ea614a94565b600082116040518060600160405280602e815260200161584b602e9139906137255760405162461bcd60e51b8152600401610b3591906150af565b50506040805160a0810182529485528051808201825293845260006020858101829052808701959095528151808301835293845283850152848101929092528151928301909152600182526060830191909152608082015290565b600e8201541515156040518060600160405280602f81526020016152e7602f9139906137bf5760405162461bcd60e51b8152600401610b3591906150af565b508051600783015560208082015180516008850155810151600984015560408201518051600a8501550151600b830155606081015151600c83015560800151600d82015542600e90910155565b600e830154421015613870578151600784015560208083015180516008860155810151600985015560408301518051600a8601550151600b840155606082015151600c8401556080820151600d8401556138668142615121565b600e840155505050565b600783018054845560088401805460018601556009850180546002870155600a860180546003880155600b870180546004890155600c8801805460058a0155600d8901805460068b01558851909655602080890151805190965594850151909355604087015180519092559201519091556060840151519055608083015190556138668142615121565b6000805160206154dc8339815191526000613913610acf565b825460408051808201909152601b8152600080516020615421833981519152602082015291925090856139595760405162461bcd60e51b8152600401610b3591906150af565b5060408051808201909152601d81527f353430327c446562743a206e6f2066756e647320617661696c61626c650000006020820152826139ac5760405162461bcd60e51b8152600401610b3591906150af565b508085111560405180606001604052806025815260200161556160259139906139e85760405162461bcd60e51b8152600401610b3591906150af565b5060006139f58387612a6b565b845481900385559050613a088582614151565b83546040805183815260208101929092527f75aec94f6ad949cfd9728eb818639f1b0b882089b480409ab73245c06469eef4910160405180910390a1505050505050565b6000613a588254151590565b6040518060400160405280601d81526020017f353534317c54696d6572733a2074696d6572206e6f742061637469766500000081525090613aac5760405162461bcd60e51b8152600401610b3591906150af565b505054421190565b6000805160206154dc833981519152805460408051808201909152601b8152600080516020615421833981519152602082015283613b055760405162461bcd60e51b8152600401610b3591906150af565b50808311156040518060600160405280602581526020016155616025913990613b415760405162461bcd60e51b8152600401610b3591906150af565b5081548390038083556040805185815260208101929092527fb940258db5193d85cafbc591d0c29fad88d804ff8b9ab71c8f9545af2bb2c07c9101612e6c565b60008360010160008481526020019081526020016000205490506000811160405180606001604052806025815260200161551c6025913990613bd65760405162461bcd60e51b8152600401610b3591906150af565b508082146040518060600160405280602581526020016158796025913990613c115760405162461bcd60e51b8152600401610b3591906150af565b5083600001548111156040518060600160405280602a8152602001615712602a913990613c515760405162461bcd60e51b8152600401610b3591906150af565b50835403835550600090815260019091016020526040812055565b613c76848361415b565b156129da57613c886001850184614199565b806129da576129da6003850184614199565b805460018201546000919080821015613cb4576000612901565b900392915050565b613cc881600501613a4c565b156110c857600060028201556000600482015560068101546110c8906005830190613e09565b6115d760018301826141e8565b6115d760038301826141e8565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b179052612aa5908490614205565b60006040518060600160405280602281526020016156b56022913980516020918201208351604051613d959301918252602082015260400190565b604051602081830303815290604052805190602001209050919050565b60006129f9613dbf614268565b8360405161190160f01b8152600281019290925260228201526042902090565b600080600080613def8686614272565b925092509250613dff82826142bf565b5090949350505050565b60408051808201909152601781527f353534307c54696d6572733a207a65726f2064656c6179000000000000000000602082015281613e5b5760405162461bcd60e51b8152600401610b3591906150af565b50613e668142615121565b90915550565b8254612aa59060018501906001600160a01b03168484614378565b601b5442101580613e955750815b15613eb057612aa58382613ea9600d6129e0565b9190614500565b613ebc600d8483614500565b612aa560148483614500565b613ed18361295b565b60008215613f3c576000613ee5600161284d565b9050848110613ef657849150613f36565b809150857f83067e3478aa8b638f7e34eeecb23875cc3671e615445e892cc22dcd4c8b8df3828703604051613f2d91815260200190565b60405180910390a25b50613f8e565b613f44610acf565b8411156040518060400160405280601f81526020016000805160206154bc83398151915281525090613f895760405162461bcd60e51b8152600401610b3591906150af565b508390505b60408051808201909152601f81527f353331337c4163636f756e743a206e6f2066756e647320746f20626c6f636b00602082015281613fe05760405162461bcd60e51b8152600401610b3591906150af565b50604080516060810182528281526001600160801b03428116602080840191825286151584860190815260008b815260059092529481209351845590516001909301805494511515600160801b026001600160881b03199095169390921692909217929092179091556007805483929061405b908490615121565b90915550505050505050565b806006541015614078576000614086565b80600654614086919061510e565b6006556000546115d7906001600160a01b03168383613d08565b6140a982614562565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a28051156140ee57612aa582826145c7565b6115d761463d565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff1661103257604051631afcd79f60e31b815260040160405180910390fd5b6141476140f6565b6115d7828261465c565b6115d78282612b0b565b60006005830161416a81613a4c565b156141795760009150506129f9565b6006840154815460009161418c9161510e565b84101592506129f9915050565b80826001015410156040518060600160405280602e8152602001615798602e9139906141d85760405162461bcd60e51b8152600401610b3591906150af565b5060019091018054919091039055565b808260010160008282546141fc9190615121565b90915550505050565b600061421a6001600160a01b038416836146bd565b9050805160001415801561423f57508080602001905181019061423d919061514d565b155b15612aa557604051635274afe760e01b81526001600160a01b0384166004820152602401610b35565b6000610adb6146cb565b600080600083516041036142ac5760208401516040850151606086015160001a61429e8882858561473f565b9550955095505050506142b8565b50508151600091506002905b9250925092565b60008260038111156142d3576142d36151a4565b036142dc575050565b60018260038111156142f0576142f06151a4565b0361430e5760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115614322576143226151a4565b036143435760405163fce698f760e01b815260048101829052602401610b35565b6003826003811115614357576143576151a4565b036115d7576040516335e2f38360e21b815260048101829052602401610b35565b83546040805160608101909152602980825261ffff600160801b90930483169284168311919061539e6020830139906143c45760405162461bcd60e51b8152600401610b3591906150af565b5082825b8261ffff168161ffff161015614482576040516bffffffffffffffffffffffff19606088901b1660208201526001600160f01b031960f083901b1660348201526001600160801b03198316603682015260029060460160408051601f1981840301815290829052614438916151ba565b602060405180830381855afa158015614455573d6000803e3d6000fd5b5050506040513d601f19601f8201168201806040525081019061447891906150df565b91506001016143c8565b5085546040805160608101909152602180825260809290921b6001600160801b031990811690841614916155ae6020830139906144d25760405162461bcd60e51b8152600401610b3591906150af565b5050845461ffff909216600160801b026001600160901b031990921660809390931c92909217179092555050565b82600001548211156040518060600160405280603b815260200161561e603b91399061453f5760405162461bcd60e51b8152600401610b3591906150af565b5061454983613cbc565b614553838361480e565b80612aa557612aa5838361485d565b806001600160a01b03163b60000361459857604051634c9c8ce360e01b81526001600160a01b0382166004820152602401610b35565b6000805160206155cf83398151915280546001600160a01b0319166001600160a01b0392909216919091179055565b6060600080846001600160a01b0316846040516145e491906151ba565b600060405180830381855af49150503d806000811461461f576040519150601f19603f3d011682016040523d82523d6000602084013e614624565b606091505b50915091506146348583836148af565b95945050505050565b34156110325760405163b398979f60e01b815260040160405180910390fd5b6146646140f6565b6000805160206155418339815191527fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d10261469e8482615226565b50600381016146ad8382615226565b5060008082556001909101555050565b6060612a7c8383600061490b565b60007f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f6146f66149a8565b6146fe614a12565b60408051602081019490945283019190915260608201524660808201523060a082015260c00160405160208183030381529060405280519060200120905090565b600080807f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a084111561477a5750600091506003905082614804565b604080516000808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa1580156147ce573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b0381166147fa57506000925060019150829050614804565b9250600091508190505b9450945094915050565b61481b6001830182614a56565b60405180606001604052806028815260200161558660289139906148525760405162461bcd60e51b8152600401610b3591906150af565b506115d78282613cee565b61486a6003830182614a56565b6040518060600160405280602f81526020016155ef602f9139906148a15760405162461bcd60e51b8152600401610b3591906150af565b506115d760038301826141e8565b6060826148c4576148bf82614a6b565b612a7c565b81511580156148db57506001600160a01b0384163b155b1561490457604051639996b31560e01b81526001600160a01b0385166004820152602401610b35565b5092915050565b6060814710156149305760405163cd78605960e01b8152306004820152602401610b35565b600080856001600160a01b0316848660405161494c91906151ba565b60006040518083038185875af1925050503d8060008114614989576040519150601f19603f3d011682016040523d82523d6000602084013e61498e565b606091505b509150915061499e8683836148af565b9695505050505050565b6000600080516020615541833981519152816149c26135c6565b8051909150156149da57805160209091012092915050565b815480156149e9579392505050565b7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470935050505090565b600060008051602061554183398151915281614a2c613689565b805190915015614a4457805160209091012092915050565b600182015480156149e9579392505050565b6000614a6183613c9a565b9091111592915050565b805115614a7b5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b6040518060a0016040528060008152602001614ac3604051806040016040528060008152602001600081525090565b8152602001614ae5604051806040016040528060008152602001600081525090565b8152602001614b006040518060200160405280600081525090565b8152602001600081525090565b60008060408385031215614b2057600080fd5b50508035926020909101359150565b6001600160a01b03811681146110c857600080fd5b60008083601f840112614b5657600080fd5b50813567ffffffffffffffff811115614b6e57600080fd5b602083019150836020828501011115614b8657600080fd5b9250929050565b600080600060408486031215614ba257600080fd5b8335614bad81614b2f565b9250602084013567ffffffffffffffff811115614bc957600080fd5b614bd586828701614b44565b9497909650939450505050565b600060208284031215614bf457600080fd5b5035919050565b80356001600160801b031981168114614c1357600080fd5b919050565b803561ffff81168114614c1357600080fd5b600080600080600060808688031215614c4257600080fd5b8535614c4d81614b2f565b9450614c5b60208701614bfb565b9350614c6960408701614c18565b9250606086013567ffffffffffffffff811115614c8557600080fd5b614c9188828901614b44565b969995985093965092949392505050565b80151581146110c857600080fd5b600080600080600060a08688031215614cc857600080fd5b8535945060208601359350614cdf60408701614bfb565b9250614ced60608701614c18565b91506080860135614cfd81614ca2565b809150509295509295909350565b600060208284031215614d1d57600080fd5b8135612a7c81614b2f565b634e487b7160e01b600052604160045260246000fd5b60008060408385031215614d5157600080fd5b8235614d5c81614b2f565b9150602083013567ffffffffffffffff80821115614d7957600080fd5b818501915085601f830112614d8d57600080fd5b813581811115614d9f57614d9f614d28565b604051601f8201601f19908116603f01168101908382118183101715614dc757614dc7614d28565b81604052828152886020848701011115614de057600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b600080600060608486031215614e1757600080fd5b83359250602084013591506040840135614e3081614ca2565b809150509250925092565b60005b83811015614e56578181015183820152602001614e3e565b50506000910152565b60008151808452614e77816020860160208601614e3b565b601f01601f19169290920160200192915050565b60ff60f81b881681526000602060e06020840152614eac60e084018a614e5f565b8381036040850152614ebe818a614e5f565b606085018990526001600160a01b038816608086015260a0850187905284810360c08601528551808252602080880193509091019060005b81811015614f1257835183529284019291840191600101614ef6565b50909c9b505050505050505050505050565b8051825260208082015180518285015290810151604084015250604081015180516060840152602081015160808401525060608101515160a08301526080015160c090910152565b6101e08101614f7b8286614f24565b614f8860e0830185614f24565b826101c0830152949350505050565b60008060008060008060c08789031215614fb057600080fd5b8635614fbb81614b2f565b95506020870135614fcb81614b2f565b95989597505050506040840135936060810135936080820135935060a0909101359150565b6000806040838503121561500357600080fd5b61500c83614bfb565b915061501a60208401614c18565b90509250929050565b6000806000806080858703121561503957600080fd5b5050823594602084013594506040840135936060013592509050565b80516001600160a01b0316825260209081015180516001600160801b03191682840152015161ffff16604090910152565b60e081016150948286615055565b6150a16060830185615055565b8260c0830152949350505050565b602081526000612a7c6020830184614e5f565b6000602082840312156150d457600080fd5b8151612a7c81614b2f565b6000602082840312156150f157600080fd5b5051919050565b634e487b7160e01b600052601160045260246000fd5b818103818111156129f9576129f96150f8565b808201808211156129f9576129f96150f8565b600060018201615146576151466150f8565b5060010190565b60006020828403121561515f57600080fd5b8151612a7c81614ca2565b600181811c9082168061517e57607f821691505b60208210810361519e57634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052602160045260246000fd5b600082516151cc818460208701614e3b565b9190910192915050565b601f821115612aa5576000816000526020600020601f850160051c810160208610156151ff5750805b601f850160051c820191505b8181101561521e5782815560010161520b565b505050505050565b815167ffffffffffffffff81111561524057615240614d28565b6152548161524e845461516a565b846151d6565b602080601f83116001811461528957600084156152715750858301515b600019600386901b1c1916600185901b17855561521e565b600085815260208120601f198616915b828110156152b857888601518255948401946001909101908401615299565b50858210156152d65787850151600019600388901b60f8161c191681555b5050505050600190811b0190555056fe353530307c44656c6179656453657474696e67733a2076616c756520616c726561647920696e697469616c697a6564353330327c4163636f756e743a206f6e6c792070726f636573736f722063616e2063616c6c20746869732066756e6374696f6e353334307c4163636f756e743a20736574746c656d656e7420706572696f64206973206e6f74206f766572353432307c526566756e64733a20726566756e64207265636f726420616c726561647920657869737473353531307c4f6e6554696d6550617373776f72643a20696e76616c6964204f545020636f756e746572353335307c4163636f756e743a20726573756c74696e672076657269666965642062616c616e636520657863656564732061637475616c353335317c4163636f756e743a2070726f636573736f7220616c726561647920736574353130307c436f6d6d6f6e3a20616d6f756e74206973207a65726f0000000000353431307c44656c617965645769746864726177616c733a207769746864726177616c2074696d656c6f636b206e6f742065787072696564353332307c4163636f756e743a207472616e73616374696f6e206e6f7420666f756e6476d8021c4b979e99674b80f4e0cc10154af0e5f32df05eb51b29a464e2435500353130317c436f6d6d6f6e3a20696e73756666696369656e742066756e6473003486e95df892f06bd37fd38c44e2fee4c4efb6660a66e6eb20ed6d8a167eae0076d8021c4b979e99674b80f4e0cc10154af0e5f32df05eb51b29a464e2435501353432317c526566756e64733a20726566756e64207265636f7264206e6f7420666f756e64a16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100353430337c446562743a20616d6f756e7420697320686967686572207468616e2064656274353532317c4163636f756e744c696d6974733a207370656e64206c696d6974206578636565646564353531317c4f6e6554696d6550617373776f72643a20696e76616c6964204f5450360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc353532337c4163636f756e744c696d6974733a206e6f206f7470207370656e64206c696d6974206578636565646564353532307c4163636f756e744c696d6974733a20616d6f756e7420657863656564732073696e676c65207472616e73616374696f6e206c696d6974353331327c4163636f756e743a2063617264206f72204f5450207374617465206368616e676520697320696e2070726f6772657373353530317c44656c6179656453657474696e67733a2076616c756520616c7265616479207365744f776e657273686970416363657074616e63652875696e74323536206e6f6e636529353332317c4163636f756e743a206e657720616d6f756e7420697320657175616c206f7220686967686572207468616e20617574686f72697a6564353432337c526566756e64733a2070656e64696e6720616d6f756e74206578636565647320746f74616c353331307c4163636f756e743a207472616e73616374696f6e20494420616c72656164792075736564353333307c4163636f756e743a2076657269666965642062616c616e6365206e6f7420657175616c20746f2062616c616e6365353533317c5370656e644c696d6974733a20696e73756666696369656e74207370656e7420746f2063616e63656c353335327c4163636f756e743a20636172642077697468206f7470207374617465206e6f7420696e697469616c697a6564353535307c4f776e657273686970416363657074616e63653a20696e76616c6964207369676e6174757265353330347c4163636f756e743a20696e73756666696369656e7420626c6f636b656420616d6f756e74353532347c4163636f756e744c696d6974733a207370656e64206c696d697420706572696f64206973207a65726f353432327c526566756e64733a20696e76616c696420616d6f756e74207265636569766564353330317c4163636f756e743a206f6e6c79206f776e65722063616e2063616c6c20746869732066756e6374696f6e353337307c4163636f756e743a20756e617574686f72697a656420696d706c656d656e746174696f6ea26469706673582212201bf402f34a1057d0d739b2911423245677cc744b072d4af8e7f665bc51781a7f64736f6c63430008160033"; public static final String FUNC_UPGRADE_INTERFACE_VERSION = "UPGRADE_INTERFACE_VERSION"; @@ -59,6 +60,8 @@ class TangemPaymentAccount extends Contract { public static final String FUNC_DEBTAMOUNT = "debtAmount"; + public static final String FUNC_EIP712DOMAIN = "eip712Domain"; + public static final String FUNC_FACTORY = "factory"; public static final String FUNC_INCREASEVERIFIEDBALANCE = "increaseVerifiedBalance"; @@ -75,7 +78,9 @@ class TangemPaymentAccount extends Contract { public static final String FUNC_LIMITS = "limits"; - public static final String FUNC_OWNERWALLET = "ownerWallet"; + public static final String FUNC_OWNER = "owner"; + + public static final String FUNC_OWNERSHIPACCEPTANCENONCE = "ownershipAcceptanceNonce"; public static final String FUNC_PAYMENTTOKEN = "paymentToken"; @@ -103,6 +108,8 @@ class TangemPaymentAccount extends Contract { public static final String FUNC_PROXIABLEUUID = "proxiableUUID"; + public static final String FUNC_REGISTRY = "registry"; + public static final String FUNC_SAVEPENDINGREFUND = "savePendingRefund"; public static final String FUNC_SECURITYDELAY = "securityDelay"; @@ -113,7 +120,7 @@ class TangemPaymentAccount extends Contract { public static final String FUNC_SETOTPSTATE = "setOtpState"; - public static final String FUNC_SETOWNERWALLET = "setOwnerWallet"; + public static final String FUNC_SETOWNER = "setOwner"; public static final String FUNC_SETPROCESSOR = "setProcessor"; @@ -163,6 +170,9 @@ class TangemPaymentAccount extends Contract { }, new TypeReference() { })); + public static final Event EIP712DOMAINCHANGED_EVENT = new Event("EIP712DomainChanged", + List.of()); + public static final Event INITIALIZED_EVENT = new Event("Initialized", List.of(new TypeReference() { })); @@ -189,7 +199,7 @@ class TangemPaymentAccount extends Contract { }, new TypeReference() { })); - public static final Event OWNERWALLETSET_EVENT = new Event("OwnerWalletSet", + public static final Event OWNERSET_EVENT = new Event("OwnerSet", List.of(new TypeReference

() { })); @@ -222,7 +232,6 @@ class TangemPaymentAccount extends Contract { public static final Event TRANSACTIONSETTLED_EVENT = new Event("TransactionSettled", Arrays.asList(new TypeReference(true) { - }, new TypeReference(true) { }, new TypeReference() { }, new TypeReference() { })); @@ -302,6 +311,25 @@ class TangemPaymentAccount extends Contract { return typedResponse; } + public static CardSetEventResponse getCardSetEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(CARDSET_EVENT, log); + CardSetEventResponse typedResponse = new CardSetEventResponse(); + typedResponse.log = log; + typedResponse.card = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.otpRoot = (byte[]) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.otpCounter = (BigInteger) eventValues.getNonIndexedValues().get(2).getValue(); + return typedResponse; + } + + public static DebtIncreasedEventResponse getDebtIncreasedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTINCREASED_EVENT, log); + DebtIncreasedEventResponse typedResponse = new DebtIncreasedEventResponse(); + typedResponse.log = log; + typedResponse.increase = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.debtAmount = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + return typedResponse; + } + public static List getCardSetEvents(TransactionReceipt transactionReceipt) { List valueList = staticExtractEventParametersWithLog(CARDSET_EVENT, transactionReceipt); ArrayList responses = new ArrayList(valueList.size()); @@ -316,6 +344,35 @@ class TangemPaymentAccount extends Contract { return responses; } + public static DebtPaidEventResponse getDebtPaidEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTPAID_EVENT, log); + DebtPaidEventResponse typedResponse = new DebtPaidEventResponse(); + typedResponse.log = log; + typedResponse.paid = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + return typedResponse; + } + + public static DebtWrittenOffEventResponse getDebtWrittenOffEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTWRITTENOFF_EVENT, log); + DebtWrittenOffEventResponse typedResponse = new DebtWrittenOffEventResponse(); + typedResponse.log = log; + typedResponse.writtenOff = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + return typedResponse; + } + + public static List getEIP712DomainChangedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(EIP712DOMAINCHANGED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + EIP712DomainChangedEventResponse typedResponse = new EIP712DomainChangedEventResponse(); + typedResponse.log = eventValues.getLog(); + responses.add(typedResponse); + } + return responses; + } + public static List getDebtIncreasedEvents(TransactionReceipt transactionReceipt) { List valueList = staticExtractEventParametersWithLog(DEBTINCREASED_EVENT, transactionReceipt); ArrayList responses = new ArrayList(valueList.size()); @@ -329,214 +386,13 @@ class TangemPaymentAccount extends Contract { return responses; } - public static List getDebtPaidEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(DEBTPAID_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - DebtPaidEventResponse typedResponse = new DebtPaidEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.paid = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static CardSetEventResponse getCardSetEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(CARDSET_EVENT, log); - CardSetEventResponse typedResponse = new CardSetEventResponse(); + public static EIP712DomainChangedEventResponse getEIP712DomainChangedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(EIP712DOMAINCHANGED_EVENT, log); + EIP712DomainChangedEventResponse typedResponse = new EIP712DomainChangedEventResponse(); typedResponse.log = log; - typedResponse.card = (String) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.otpRoot = (byte[]) eventValues.getNonIndexedValues().get(1).getValue(); - typedResponse.otpCounter = (BigInteger) eventValues.getNonIndexedValues().get(2).getValue(); return typedResponse; } - public static List getDebtWrittenOffEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(DEBTWRITTENOFF_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - DebtWrittenOffEventResponse typedResponse = new DebtWrittenOffEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.writtenOff = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getInitializedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(INITIALIZED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - InitializedEventResponse typedResponse = new InitializedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.version = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getInsufficientFundsOnForcedAuthEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(INSUFFICIENTFUNDSONFORCEDAUTH_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - InsufficientFundsOnForcedAuthEventResponse typedResponse = new InsufficientFundsOnForcedAuthEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.insufficientAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static DebtIncreasedEventResponse getDebtIncreasedEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTINCREASED_EVENT, log); - DebtIncreasedEventResponse typedResponse = new DebtIncreasedEventResponse(); - typedResponse.log = log; - typedResponse.increase = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.debtAmount = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - return typedResponse; - } - - public static List getLimitsSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(LIMITSSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - LimitsSetEventResponse typedResponse = new LimitsSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.singleTransactionLimit = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.spendLimit = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - typedResponse.noOtpSpendLimit = (BigInteger) eventValues.getNonIndexedValues().get(2).getValue(); - typedResponse.spendLimitsPeriod = (BigInteger) eventValues.getNonIndexedValues().get(3).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getNoOtpTransactionAuthorizedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(NOOTPTRANSACTIONAUTHORIZED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - NoOtpTransactionAuthorizedEventResponse typedResponse = new NoOtpTransactionAuthorizedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getOtpStateSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(OTPSTATESET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - OtpStateSetEventResponse typedResponse = new OtpStateSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.otpRoot = (byte[]) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.otpCounter = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static DebtPaidEventResponse getDebtPaidEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTPAID_EVENT, log); - DebtPaidEventResponse typedResponse = new DebtPaidEventResponse(); - typedResponse.log = log; - typedResponse.paid = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - return typedResponse; - } - - public static List getOwnerWalletSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(OWNERWALLETSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - OwnerWalletSetEventResponse typedResponse = new OwnerWalletSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.ownerWallet = (String) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getPendingRefundSavedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(PENDINGREFUNDSAVED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - PendingRefundSavedEventResponse typedResponse = new PendingRefundSavedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getProcessorSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(PROCESSORSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - ProcessorSetEventResponse typedResponse = new ProcessorSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.processor = (String) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.paymentToken = (String) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static DebtWrittenOffEventResponse getDebtWrittenOffEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTWRITTENOFF_EVENT, log); - DebtWrittenOffEventResponse typedResponse = new DebtWrittenOffEventResponse(); - typedResponse.log = log; - typedResponse.writtenOff = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - return typedResponse; - } - - public static List getRefundPaidEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(REFUNDPAID_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - RefundPaidEventResponse typedResponse = new RefundPaidEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getTransactionAmountChangedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(TRANSACTIONAMOUNTCHANGED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - TransactionAmountChangedEventResponse typedResponse = new TransactionAmountChangedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.newAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getTransactionAuthorizedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(TRANSACTIONAUTHORIZED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - TransactionAuthorizedEventResponse typedResponse = new TransactionAuthorizedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.otp = (byte[]) eventValues.getNonIndexedValues().get(1).getValue(); - typedResponse.otpCounter = (BigInteger) eventValues.getNonIndexedValues().get(2).getValue(); - responses.add(typedResponse); - } - return responses; - } - public static InitializedEventResponse getInitializedEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(INITIALIZED_EVENT, log); InitializedEventResponse typedResponse = new InitializedEventResponse(); @@ -545,46 +401,6 @@ class TangemPaymentAccount extends Contract { return typedResponse; } - public static List getTransactionSettledEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(TRANSACTIONSETTLED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - TransactionSettledEventResponse typedResponse = new TransactionSettledEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.settlementId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.settlementAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.paymentAmount = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getUnsettledTransactionUnblockedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(UNSETTLEDTRANSACTIONUNBLOCKED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - UnsettledTransactionUnblockedEventResponse typedResponse = new UnsettledTransactionUnblockedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getUpgradedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(UPGRADED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - UpgradedEventResponse typedResponse = new UpgradedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.implementation = (String) eventValues.getIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - public static InsufficientFundsOnForcedAuthEventResponse getInsufficientFundsOnForcedAuthEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(INSUFFICIENTFUNDSONFORCEDAUTH_EVENT, log); InsufficientFundsOnForcedAuthEventResponse typedResponse = new InsufficientFundsOnForcedAuthEventResponse(); @@ -594,37 +410,14 @@ class TangemPaymentAccount extends Contract { return typedResponse; } - public static List getVerifiedBalanceIncreasedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCEINCREASED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); + public static List getDebtPaidEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(DEBTPAID_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); for (EventValuesWithLog eventValues : valueList) { - VerifiedBalanceIncreasedEventResponse typedResponse = new VerifiedBalanceIncreasedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.increase = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getVerifiedBalanceSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCESET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - VerifiedBalanceSetEventResponse typedResponse = new VerifiedBalanceSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getWithdrawalCanceledEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(WITHDRAWALCANCELED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - WithdrawalCanceledEventResponse typedResponse = new WithdrawalCanceledEventResponse(); + DebtPaidEventResponse typedResponse = new DebtPaidEventResponse(); typedResponse.log = eventValues.getLog(); + typedResponse.paid = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); responses.add(typedResponse); } return responses; @@ -641,37 +434,6 @@ class TangemPaymentAccount extends Contract { return typedResponse; } - public static List getWithdrawalCompleteEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(WITHDRAWALCOMPLETE_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - WithdrawalCompleteEventResponse typedResponse = new WithdrawalCompleteEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.to = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getWithdrawalInitiatedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(WITHDRAWALINITIATED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - WithdrawalInitiatedEventResponse typedResponse = new WithdrawalInitiatedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.readyToWithdrawTimestamp = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - @Deprecated - public static TangemPaymentAccount load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { - return new TangemPaymentAccount(contractAddress, web3j, credentials, gasPrice, gasLimit); - } - public static NoOtpTransactionAuthorizedEventResponse getNoOtpTransactionAuthorizedEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(NOOTPTRANSACTIONAUTHORIZED_EVENT, log); NoOtpTransactionAuthorizedEventResponse typedResponse = new NoOtpTransactionAuthorizedEventResponse(); @@ -681,19 +443,6 @@ class TangemPaymentAccount extends Contract { return typedResponse; } - @Deprecated - public static TangemPaymentAccount load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { - return new TangemPaymentAccount(contractAddress, web3j, transactionManager, gasPrice, gasLimit); - } - - public static TangemPaymentAccount load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { - return new TangemPaymentAccount(contractAddress, web3j, credentials, contractGasProvider); - } - - public static TangemPaymentAccount load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { - return new TangemPaymentAccount(contractAddress, web3j, transactionManager, contractGasProvider); - } - public static OtpStateSetEventResponse getOtpStateSetEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(OTPSTATESET_EVENT, log); OtpStateSetEventResponse typedResponse = new OtpStateSetEventResponse(); @@ -703,36 +452,39 @@ class TangemPaymentAccount extends Contract { return typedResponse; } - public static RemoteCall deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider, String trustedForwarder) { - String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); - return deployRemoteCall(TangemPaymentAccount.class, web3j, credentials, contractGasProvider, BINARY, encodedConstructor); + public static List getDebtWrittenOffEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(DEBTWRITTENOFF_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + DebtWrittenOffEventResponse typedResponse = new DebtWrittenOffEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.writtenOff = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; } - public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider, String trustedForwarder) { - String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); - return deployRemoteCall(TangemPaymentAccount.class, web3j, transactionManager, contractGasProvider, BINARY, encodedConstructor); + public static List getOwnerSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(OWNERSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + OwnerSetEventResponse typedResponse = new OwnerSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.owner = (String) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; } - @Deprecated - public static RemoteCall deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit, String trustedForwarder) { - String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); - return deployRemoteCall(TangemPaymentAccount.class, web3j, credentials, gasPrice, gasLimit, BINARY, encodedConstructor); - } - - public static OwnerWalletSetEventResponse getOwnerWalletSetEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(OWNERWALLETSET_EVENT, log); - OwnerWalletSetEventResponse typedResponse = new OwnerWalletSetEventResponse(); + public static OwnerSetEventResponse getOwnerSetEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(OWNERSET_EVENT, log); + OwnerSetEventResponse typedResponse = new OwnerSetEventResponse(); typedResponse.log = log; - typedResponse.ownerWallet = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.owner = (String) eventValues.getNonIndexedValues().get(0).getValue(); return typedResponse; } - @Deprecated - public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit, String trustedForwarder) { - String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); - return deployRemoteCall(TangemPaymentAccount.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, encodedConstructor); - } - public static PendingRefundSavedEventResponse getPendingRefundSavedEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PENDINGREFUNDSAVED_EVENT, log); PendingRefundSavedEventResponse typedResponse = new PendingRefundSavedEventResponse(); @@ -780,12 +532,23 @@ class TangemPaymentAccount extends Contract { return typedResponse; } + public static List getInitializedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(INITIALIZED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + InitializedEventResponse typedResponse = new InitializedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.version = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public static TransactionSettledEventResponse getTransactionSettledEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(TRANSACTIONSETTLED_EVENT, log); TransactionSettledEventResponse typedResponse = new TransactionSettledEventResponse(); typedResponse.log = log; typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.settlementId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); typedResponse.settlementAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); typedResponse.paymentAmount = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); return typedResponse; @@ -808,6 +571,19 @@ class TangemPaymentAccount extends Contract { return typedResponse; } + public static List getInsufficientFundsOnForcedAuthEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(INSUFFICIENTFUNDSONFORCEDAUTH_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + InsufficientFundsOnForcedAuthEventResponse typedResponse = new InsufficientFundsOnForcedAuthEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.insufficientAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public static VerifiedBalanceIncreasedEventResponse getVerifiedBalanceIncreasedEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(VERIFIEDBALANCEINCREASED_EVENT, log); VerifiedBalanceIncreasedEventResponse typedResponse = new VerifiedBalanceIncreasedEventResponse(); @@ -832,6 +608,21 @@ class TangemPaymentAccount extends Contract { return typedResponse; } + public static List getLimitsSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(LIMITSSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + LimitsSetEventResponse typedResponse = new LimitsSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.singleTransactionLimit = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.spendLimit = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.noOtpSpendLimit = (BigInteger) eventValues.getNonIndexedValues().get(2).getValue(); + typedResponse.spendLimitsPeriod = (BigInteger) eventValues.getNonIndexedValues().get(3).getValue(); + responses.add(typedResponse); + } + return responses; + } + public static WithdrawalCompleteEventResponse getWithdrawalCompleteEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(WITHDRAWALCOMPLETE_EVENT, log); WithdrawalCompleteEventResponse typedResponse = new WithdrawalCompleteEventResponse(); @@ -841,13 +632,70 @@ class TangemPaymentAccount extends Contract { return typedResponse; } - public static WithdrawalInitiatedEventResponse getWithdrawalInitiatedEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(WITHDRAWALINITIATED_EVENT, log); - WithdrawalInitiatedEventResponse typedResponse = new WithdrawalInitiatedEventResponse(); - typedResponse.log = log; - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.readyToWithdrawTimestamp = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - return typedResponse; + @Deprecated + public static TangemPaymentAccount load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + return new TangemPaymentAccount(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + @Deprecated + public static TangemPaymentAccount load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + return new TangemPaymentAccount(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + public static List getNoOtpTransactionAuthorizedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(NOOTPTRANSACTIONAUTHORIZED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + NoOtpTransactionAuthorizedEventResponse typedResponse = new NoOtpTransactionAuthorizedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static TangemPaymentAccount load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + return new TangemPaymentAccount(contractAddress, web3j, credentials, contractGasProvider); + } + + public static TangemPaymentAccount load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return new TangemPaymentAccount(contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static RemoteCall deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider, String trustedForwarder) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); + return deployRemoteCall(TangemPaymentAccount.class, web3j, credentials, contractGasProvider, BINARY, encodedConstructor); + } + + public static List getOtpStateSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(OTPSTATESET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + OtpStateSetEventResponse typedResponse = new OtpStateSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.otpRoot = (byte[]) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.otpCounter = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider, String trustedForwarder) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); + return deployRemoteCall(TangemPaymentAccount.class, web3j, transactionManager, contractGasProvider, BINARY, encodedConstructor); + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit, String trustedForwarder) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); + return deployRemoteCall(TangemPaymentAccount.class, web3j, credentials, gasPrice, gasLimit, BINARY, encodedConstructor); + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit, String trustedForwarder) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); + return deployRemoteCall(TangemPaymentAccount.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, encodedConstructor); } public Flowable accountStateAfterSettlementEventFlowable(EthFilter filter) { @@ -870,6 +718,19 @@ class TangemPaymentAccount extends Contract { return cardSetEventFlowable(filter); } + public static List getPendingRefundSavedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PENDINGREFUNDSAVED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PendingRefundSavedEventResponse typedResponse = new PendingRefundSavedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable debtIncreasedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getDebtIncreasedEventFromLog(log)); } @@ -884,6 +745,19 @@ class TangemPaymentAccount extends Contract { return web3j.ethLogFlowable(filter).map(log -> getDebtPaidEventFromLog(log)); } + public static List getProcessorSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PROCESSORSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + ProcessorSetEventResponse typedResponse = new ProcessorSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.processor = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.paymentToken = (String) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable debtPaidEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(DEBTPAID_EVENT)); @@ -900,10 +774,46 @@ class TangemPaymentAccount extends Contract { return debtWrittenOffEventFlowable(filter); } + public static List getRefundPaidEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(REFUNDPAID_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + RefundPaidEventResponse typedResponse = new RefundPaidEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public Flowable eIP712DomainChangedEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getEIP712DomainChangedEventFromLog(log)); + } + + public Flowable eIP712DomainChangedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(EIP712DOMAINCHANGED_EVENT)); + return eIP712DomainChangedEventFlowable(filter); + } + public Flowable initializedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getInitializedEventFromLog(log)); } + public static List getTransactionAmountChangedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(TRANSACTIONAMOUNTCHANGED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + TransactionAmountChangedEventResponse typedResponse = new TransactionAmountChangedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.newAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable initializedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(INITIALIZED_EVENT)); @@ -920,6 +830,21 @@ class TangemPaymentAccount extends Contract { return insufficientFundsOnForcedAuthEventFlowable(filter); } + public static List getTransactionAuthorizedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(TRANSACTIONAUTHORIZED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + TransactionAuthorizedEventResponse typedResponse = new TransactionAuthorizedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.otp = (byte[]) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.otpCounter = (BigInteger) eventValues.getNonIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable limitsSetEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getLimitsSetEventFromLog(log)); } @@ -934,6 +859,20 @@ class TangemPaymentAccount extends Contract { return web3j.ethLogFlowable(filter).map(log -> getNoOtpTransactionAuthorizedEventFromLog(log)); } + public static List getTransactionSettledEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(TRANSACTIONSETTLED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + TransactionSettledEventResponse typedResponse = new TransactionSettledEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.settlementAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.paymentAmount = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable noOtpTransactionAuthorizedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(NOOTPTRANSACTIONAUTHORIZED_EVENT)); @@ -950,20 +889,45 @@ class TangemPaymentAccount extends Contract { return otpStateSetEventFlowable(filter); } - public Flowable ownerWalletSetEventFlowable(EthFilter filter) { - return web3j.ethLogFlowable(filter).map(log -> getOwnerWalletSetEventFromLog(log)); + public static List getUnsettledTransactionUnblockedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(UNSETTLEDTRANSACTIONUNBLOCKED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + UnsettledTransactionUnblockedEventResponse typedResponse = new UnsettledTransactionUnblockedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; } - public Flowable ownerWalletSetEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + public Flowable ownerSetEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getOwnerSetEventFromLog(log)); + } + + public Flowable ownerSetEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); - filter.addSingleTopic(EventEncoder.encode(OWNERWALLETSET_EVENT)); - return ownerWalletSetEventFlowable(filter); + filter.addSingleTopic(EventEncoder.encode(OWNERSET_EVENT)); + return ownerSetEventFlowable(filter); } public Flowable pendingRefundSavedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getPendingRefundSavedEventFromLog(log)); } + public static List getUpgradedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(UPGRADED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + UpgradedEventResponse typedResponse = new UpgradedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.implementation = (String) eventValues.getIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable pendingRefundSavedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(PENDINGREFUNDSAVED_EVENT)); @@ -980,6 +944,19 @@ class TangemPaymentAccount extends Contract { return processorSetEventFlowable(filter); } + public static List getVerifiedBalanceIncreasedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCEINCREASED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + VerifiedBalanceIncreasedEventResponse typedResponse = new VerifiedBalanceIncreasedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.increase = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable refundPaidEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getRefundPaidEventFromLog(log)); } @@ -994,6 +971,18 @@ class TangemPaymentAccount extends Contract { return web3j.ethLogFlowable(filter).map(log -> getTransactionAmountChangedEventFromLog(log)); } + public static List getVerifiedBalanceSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCESET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + VerifiedBalanceSetEventResponse typedResponse = new VerifiedBalanceSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable transactionAmountChangedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(TRANSACTIONAMOUNTCHANGED_EVENT)); @@ -1010,6 +999,17 @@ class TangemPaymentAccount extends Contract { return transactionAuthorizedEventFlowable(filter); } + public static List getWithdrawalCanceledEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(WITHDRAWALCANCELED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + WithdrawalCanceledEventResponse typedResponse = new WithdrawalCanceledEventResponse(); + typedResponse.log = eventValues.getLog(); + responses.add(typedResponse); + } + return responses; + } + public Flowable transactionSettledEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getTransactionSettledEventFromLog(log)); } @@ -1024,6 +1024,19 @@ class TangemPaymentAccount extends Contract { return web3j.ethLogFlowable(filter).map(log -> getUnsettledTransactionUnblockedEventFromLog(log)); } + public static List getWithdrawalCompleteEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(WITHDRAWALCOMPLETE_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + WithdrawalCompleteEventResponse typedResponse = new WithdrawalCompleteEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.to = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable unsettledTransactionUnblockedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(UNSETTLEDTRANSACTIONUNBLOCKED_EVENT)); @@ -1040,6 +1053,28 @@ class TangemPaymentAccount extends Contract { return upgradedEventFlowable(filter); } + public static List getWithdrawalInitiatedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(WITHDRAWALINITIATED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + WithdrawalInitiatedEventResponse typedResponse = new WithdrawalInitiatedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.readyToWithdrawTimestamp = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static WithdrawalInitiatedEventResponse getWithdrawalInitiatedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(WITHDRAWALINITIATED_EVENT, log); + WithdrawalInitiatedEventResponse typedResponse = new WithdrawalInitiatedEventResponse(); + typedResponse.log = log; + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.readyToWithdrawTimestamp = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + return typedResponse; + } + public Flowable withdrawalInitiatedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getWithdrawalInitiatedEventFromLog(log)); } @@ -1186,6 +1221,34 @@ class TangemPaymentAccount extends Contract { return executeRemoteCallSingleValueReturn(function, BigInteger.class); } + public RemoteFunctionCall>> eip712Domain() { + final Function function = new Function(FUNC_EIP712DOMAIN, + List.of(), + Arrays.asList(new TypeReference() { + }, new TypeReference() { + }, new TypeReference() { + }, new TypeReference() { + }, new TypeReference
() { + }, new TypeReference() { + }, new TypeReference>() { + })); + return new RemoteFunctionCall>>(function, + new Callable>>() { + @Override + public Tuple7> call() throws Exception { + List results = executeCallMultipleValueReturn(function); + return new Tuple7>( + (byte[]) results.get(0).getValue(), + (String) results.get(1).getValue(), + (String) results.get(2).getValue(), + (BigInteger) results.get(3).getValue(), + (String) results.get(4).getValue(), + (byte[]) results.get(5).getValue(), + convertToNative((List) results.get(6).getValue())); + } + }); + } + public RemoteFunctionCall factory() { final Function function = new Function(FUNC_FACTORY, List.of(), @@ -1210,10 +1273,11 @@ class TangemPaymentAccount extends Contract { return executeRemoteCallTransaction(function); } - public RemoteFunctionCall initialize(String ownerWallet_, BigInteger singleTransactionLimit, BigInteger spendLimit, BigInteger noOtpSpendLimit, BigInteger spendLimitsPeriod) { + public RemoteFunctionCall initialize(String owner_, String registry_, BigInteger singleTransactionLimit, BigInteger spendLimit, BigInteger noOtpSpendLimit, BigInteger spendLimitsPeriod) { final Function function = new Function( FUNC_INITIALIZE, - Arrays.asList(new Address(160, ownerWallet_), + Arrays.asList(new Address(160, owner_), + new Address(160, registry_), new Uint256(singleTransactionLimit), new Uint256(spendLimit), new Uint256(noOtpSpendLimit), @@ -1266,14 +1330,22 @@ class TangemPaymentAccount extends Contract { }); } - public RemoteFunctionCall ownerWallet() { - final Function function = new Function(FUNC_OWNERWALLET, + public RemoteFunctionCall owner() { + final Function function = new Function(FUNC_OWNER, List.of(), List.of(new TypeReference
() { })); return executeRemoteCallSingleValueReturn(function, String.class); } + public RemoteFunctionCall ownershipAcceptanceNonce() { + final Function function = new Function(FUNC_OWNERSHIPACCEPTANCENONCE, + List.of(), + List.of(new TypeReference() { + })); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + public RemoteFunctionCall paymentToken() { final Function function = new Function(FUNC_PAYMENTTOKEN, List.of(), @@ -1353,11 +1425,10 @@ class TangemPaymentAccount extends Contract { return executeRemoteCallTransaction(function); } - public RemoteFunctionCall processSettlement(BigInteger transactionId, BigInteger settlementId, BigInteger amount) { + public RemoteFunctionCall processSettlement(BigInteger transactionId, BigInteger amount) { final Function function = new Function( FUNC_PROCESSSETTLEMENT, Arrays.asList(new Uint256(transactionId), - new Uint256(settlementId), new Uint256(amount)), Collections.emptyList()); return executeRemoteCallTransaction(function); @@ -1387,6 +1458,14 @@ class TangemPaymentAccount extends Contract { return executeRemoteCallSingleValueReturn(function, byte[].class); } + public RemoteFunctionCall registry() { + final Function function = new Function(FUNC_REGISTRY, + List.of(), + List.of(new TypeReference
() { + })); + return executeRemoteCallSingleValueReturn(function, String.class); + } + public RemoteFunctionCall savePendingRefund(BigInteger transactionId, BigInteger amount) { final Function function = new Function( FUNC_SAVEPENDINGREFUND, @@ -1404,12 +1483,13 @@ class TangemPaymentAccount extends Contract { return executeRemoteCallSingleValueReturn(function, BigInteger.class); } - public RemoteFunctionCall setCard(String card, byte[] otpRoot, BigInteger otpCounter) { + public RemoteFunctionCall setCard(String card, byte[] otpRoot, BigInteger otpCounter, byte[] ownershipAcceptanceSignature) { final Function function = new Function( FUNC_SETCARD, Arrays.asList(new Address(160, card), new Bytes16(otpRoot), - new Uint16(otpCounter)), + new Uint16(otpCounter), + new org.web3j.abi.datatypes.DynamicBytes(ownershipAcceptanceSignature)), Collections.emptyList()); return executeRemoteCallTransaction(function); } @@ -1434,10 +1514,11 @@ class TangemPaymentAccount extends Contract { return executeRemoteCallTransaction(function); } - public RemoteFunctionCall setOwnerWallet(String ownerWallet_) { + public RemoteFunctionCall setOwner(String owner_, byte[] ownershipAcceptanceSignature) { final Function function = new Function( - FUNC_SETOWNERWALLET, - List.of(new Address(160, ownerWallet_)), + FUNC_SETOWNER, + Arrays.asList(new Address(160, owner_), + new org.web3j.abi.datatypes.DynamicBytes(ownershipAcceptanceSignature)), Collections.emptyList()); return executeRemoteCallTransaction(function); } @@ -1609,7 +1690,7 @@ class TangemPaymentAccount extends Contract { super(new Uint256(singleTransactionLimit), spendLimit, noOtpSpendLimit, - spendLimitsTimer, + spendLimitsTimer, new Uint256(spendLimitsPeriod)); this._00_singleTransactionLimit = singleTransactionLimit; this._01_spendLimit = spendLimit; @@ -1664,6 +1745,9 @@ class TangemPaymentAccount extends Contract { public BigInteger debtLeft; } + public static class EIP712DomainChangedEventResponse extends BaseEventResponse { + } + public static class InitializedEventResponse extends BaseEventResponse { public BigInteger version; } @@ -1696,8 +1780,8 @@ class TangemPaymentAccount extends Contract { public BigInteger otpCounter; } - public static class OwnerWalletSetEventResponse extends BaseEventResponse { - public String ownerWallet; + public static class OwnerSetEventResponse extends BaseEventResponse { + public String owner; } public static class PendingRefundSavedEventResponse extends BaseEventResponse { @@ -1737,8 +1821,6 @@ class TangemPaymentAccount extends Contract { public static class TransactionSettledEventResponse extends BaseEventResponse { public BigInteger transactionId; - public BigInteger settlementId; - public BigInteger settlementAmount; public BigInteger paymentAmount; diff --git a/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccountRegistry.java b/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccountRegistry.java new file mode 100644 index 0000000000..bcf93417f6 --- /dev/null +++ b/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccountRegistry.java @@ -0,0 +1,362 @@ +package com.tangem.lib.visa; + +import org.web3j.abi.EventEncoder; +import org.web3j.abi.FunctionEncoder; +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.*; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.DefaultBlockParameter; +import org.web3j.protocol.core.RemoteCall; +import org.web3j.protocol.core.RemoteFunctionCall; +import org.web3j.protocol.core.methods.request.EthFilter; +import org.web3j.protocol.core.methods.response.BaseEventResponse; +import org.web3j.protocol.core.methods.response.Log; +import org.web3j.protocol.core.methods.response.TransactionReceipt; +import org.web3j.tx.Contract; +import org.web3j.tx.TransactionManager; +import org.web3j.tx.gas.ContractGasProvider; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; + +import io.reactivex.Flowable; + +/** + *

Auto generated code. + *

Do not modify! + *

Please use the web3j command line tools, + * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the + * codegen module to update. + * + *

Generated with web3j version 1.5.2. + */ +@SuppressWarnings("rawtypes") +class TangemPaymentAccountRegistry extends Contract { + public static final String BINARY = "608060405234801561001057600080fd5b50604051610c47380380610c4783398101604081905261002f91610054565b600080546001600160a01b0319166001600160a01b0392909216919091179055610084565b60006020828403121561006657600080fd5b81516001600160a01b038116811461007d57600080fd5b9392505050565b610bb4806100936000396000f3fe608060405234801561001057600080fd5b506004361061007d5760003560e01c806368d7111a1161005b57806368d7111a146100f0578063747fc3701461010357806379da564f14610116578063c45a01551461013657600080fd5b80630cef7172146100825780633509c7c5146100c85780635cd26737146100dd575b600080fd5b6100ab6100903660046108fd565b6002602052600090815260409020546001600160a01b031681565b6040516001600160a01b0390911681526020015b60405180910390f35b6100db6100d6366004610918565b610149565b005b6100db6100eb366004610918565b610201565b6100db6100fe3660046108fd565b6103b0565b6100db610111366004610918565b610506565b6101296101243660046108fd565b6106db565b6040516100bf919061094b565b6000546100ab906001600160a01b031681565b600054604080516080810190915260428082526001600160a01b03909216331491610a576020830139906101995760405162461bcd60e51b81526004016101909190610998565b60405180910390fd5b506001600160a01b03811660009081526001602052604090206101bc9083610705565b50806001600160a01b0316826001600160a01b03167f65b303fd420aefd0541d9300bb10190bb8e3320a0e7c2ce4dec6b6bfb953027f60405160405180910390a35050565b6000546040516385bb392360e01b81523360048201526001600160a01b03909116906385bb392390602401602060405180830381865afa158015610249573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061026d91906109e7565b6040518060600160405280603a8152602001610b45603a9139906102a45760405162461bcd60e51b81526004016101909190610998565b506001600160a01b038216600090815260016020526040902033906102c99082610721565b6040518060600160405280603b8152602001610ad0603b9139906103005760405162461bcd60e51b81526004016101909190610998565b506001600160a01b03821660009081526001602052604090206103239082610705565b6040518060600160405280603b8152602001610ad0603b91399061035a5760405162461bcd60e51b81526004016101909190610998565b50604080516001600160a01b03808416825280861660208301528416918101919091527ffba993cae385d1da628b11b1b150a27262bca00bb2674980516b45ad7d331eaa906060015b60405180910390a1505050565b6000546040516385bb392360e01b81523360048201526001600160a01b03909116906385bb392390602401602060405180830381865afa1580156103f8573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061041c91906109e7565b6040518060600160405280603a8152602001610b45603a9139906104535760405162461bcd60e51b81526004016101909190610998565b506001600160a01b0381811660009081526002602090815260409182902054825160608101909352603a8084529316159290610b0b90830139906104aa5760405162461bcd60e51b81526004016101909190610998565b506001600160a01b03811660008181526002602052604080822080546001600160a01b03191633908117909155905190929183917f02e4debb2ab36299bfcc998345f66f73428adbbda9ef5efef399d75a67a4b78a9190a35050565b6000546040516385bb392360e01b81523360048201526001600160a01b03909116906385bb392390602401602060405180830381865afa15801561054e573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061057291906109e7565b6040518060600160405280603a8152602001610b45603a9139906105a95760405162461bcd60e51b81526004016101909190610998565b506001600160a01b0382811660009081526002602090815260409182902054825160608101909352603780845233949190911684149291610a9990830139906106055760405162461bcd60e51b81526004016101909190610998565b506001600160a01b0382811660009081526002602090815260409182902054825160608101909352603a8084529316159290610b0b908301399061065c5760405162461bcd60e51b81526004016101909190610998565b506001600160a01b03838116600081815260026020908152604080832080546001600160a01b03199081169091558786168085529382902080549091169587169586179055805194855290840192909252908201527f362e9cf018894fbf5dfe9c5cbfb25c3e3ac8c4d5beed564352f1af2e2712dfc8906060016103a3565b6001600160a01b03811660009081526001602052604090206060906106ff90610736565b92915050565b600061071a836001600160a01b038416610743565b9392505050565b600061071a836001600160a01b038416610792565b6060600061071a83610885565b600081815260018301602052604081205461078a575081546001818101845560008481526020808220909301849055845484825282860190935260409020919091556106ff565b5060006106ff565b6000818152600183016020526040812054801561087b5760006107b6600183610a09565b85549091506000906107ca90600190610a09565b905080821461082f5760008660000182815481106107ea576107ea610a2a565b906000526020600020015490508087600001848154811061080d5761080d610a2a565b6000918252602080832090910192909255918252600188019052604090208390555b855486908061084057610840610a40565b6001900381819060005260206000200160009055905585600101600086815260200190815260200160002060009055600193505050506106ff565b60009150506106ff565b6060816000018054806020026020016040519081016040528092919081815260200182805480156108d557602002820191906000526020600020905b8154815260200190600101908083116108c1575b50505050509050919050565b80356001600160a01b03811681146108f857600080fd5b919050565b60006020828403121561090f57600080fd5b61071a826108e1565b6000806040838503121561092b57600080fd5b610934836108e1565b9150610942602084016108e1565b90509250929050565b6020808252825182820181905260009190848201906040850190845b8181101561098c5783516001600160a01b031683529284019291840191600101610967565b50909695505050505050565b60006020808352835180602085015260005b818110156109c6578581018301518582016040015282016109aa565b506000604082860101526040601f19601f8301168501019250505092915050565b6000602082840312156109f957600080fd5b8151801515811461071a57600080fd5b818103818111156106ff57634e487b7160e01b600052601160045260246000fd5b634e487b7160e01b600052603260045260246000fd5b634e487b7160e01b600052603160045260246000fdfe353630307c52656769737472793a206f6e6c79207061796d656e74206163636f756e7420666163746f72792063616e2063616c6c20746869732066756e6374696f6e353630347c52656769737472793a2070726576696f757320636172642773207061796d656e74206163636f756e74206d69736d61746368353630327c52656769737472793a207061796d656e74206163636f756e74206265696e672072656d6f766564206e6f7420696e2074686520736574353630347c52656769737472793a207061796d656e74206163636f756e7420697320616c72656164792073657420666f72207468652063617264353630317c52656769737472793a206f6e6c79207061796d656e74206163636f756e742063616e2063616c6c20746869732066756e6374696f6ea26469706673582212203c97923f6f14a45f904087dcdced3b09f33cd32821438cd9cbf0add5824acaf064736f6c63430008160033"; + + public static final String FUNC_CHANGEPAYMENTACCOUNTCARD = "changePaymentAccountCard"; + + public static final String FUNC_CHANGEPAYMENTACCOUNTOWNER = "changePaymentAccountOwner"; + + public static final String FUNC_FACTORY = "factory"; + + public static final String FUNC_INITPAYMENTACCOUNTCARD = "initPaymentAccountCard"; + + public static final String FUNC_INITPAYMENTACCOUNTOWNER = "initPaymentAccountOwner"; + + public static final String FUNC_PAYMENTACCOUNTBYCARD = "paymentAccountByCard"; + + public static final String FUNC_PAYMENTACCOUNTSBYOWNER = "paymentAccountsByOwner"; + + public static final Event PAYMENTACCOUNTCARDCHANGED_EVENT = new Event("PaymentAccountCardChanged", + Arrays.asList(new TypeReference

() { + }, new TypeReference
() { + }, new TypeReference
() { + })); + + public static final Event PAYMENTACCOUNTCARDREGISTERED_EVENT = new Event("PaymentAccountCardRegistered", + Arrays.asList(new TypeReference
(true) { + }, new TypeReference
(true) { + })); + + public static final Event PAYMENTACCOUNTOWNERCHANGED_EVENT = new Event("PaymentAccountOwnerChanged", + Arrays.asList(new TypeReference
() { + }, new TypeReference
() { + }, new TypeReference
() { + })); + + public static final Event PAYMENTACCOUNTOWNERREGISTERED_EVENT = new Event("PaymentAccountOwnerRegistered", + Arrays.asList(new TypeReference
(true) { + }, new TypeReference
(true) { + })); + + @Deprecated + protected TangemPaymentAccountRegistry(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + protected TangemPaymentAccountRegistry(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, credentials, contractGasProvider); + } + + @Deprecated + protected TangemPaymentAccountRegistry(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + protected TangemPaymentAccountRegistry(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static List getPaymentAccountCardChangedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAYMENTACCOUNTCARDCHANGED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PaymentAccountCardChangedEventResponse typedResponse = new PaymentAccountCardChangedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.previousCard = (String) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.newCard = (String) eventValues.getNonIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static PaymentAccountCardChangedEventResponse getPaymentAccountCardChangedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PAYMENTACCOUNTCARDCHANGED_EVENT, log); + PaymentAccountCardChangedEventResponse typedResponse = new PaymentAccountCardChangedEventResponse(); + typedResponse.log = log; + typedResponse.paymentAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.previousCard = (String) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.newCard = (String) eventValues.getNonIndexedValues().get(2).getValue(); + return typedResponse; + } + + public static List getPaymentAccountCardRegisteredEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAYMENTACCOUNTCARDREGISTERED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PaymentAccountCardRegisteredEventResponse typedResponse = new PaymentAccountCardRegisteredEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.card = (String) eventValues.getIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static PaymentAccountCardRegisteredEventResponse getPaymentAccountCardRegisteredEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PAYMENTACCOUNTCARDREGISTERED_EVENT, log); + PaymentAccountCardRegisteredEventResponse typedResponse = new PaymentAccountCardRegisteredEventResponse(); + typedResponse.log = log; + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.card = (String) eventValues.getIndexedValues().get(1).getValue(); + return typedResponse; + } + + public static List getPaymentAccountOwnerChangedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAYMENTACCOUNTOWNERCHANGED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PaymentAccountOwnerChangedEventResponse typedResponse = new PaymentAccountOwnerChangedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.previousOwner = (String) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.newOwner = (String) eventValues.getNonIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static PaymentAccountOwnerChangedEventResponse getPaymentAccountOwnerChangedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PAYMENTACCOUNTOWNERCHANGED_EVENT, log); + PaymentAccountOwnerChangedEventResponse typedResponse = new PaymentAccountOwnerChangedEventResponse(); + typedResponse.log = log; + typedResponse.paymentAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.previousOwner = (String) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.newOwner = (String) eventValues.getNonIndexedValues().get(2).getValue(); + return typedResponse; + } + + public static List getPaymentAccountOwnerRegisteredEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAYMENTACCOUNTOWNERREGISTERED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PaymentAccountOwnerRegisteredEventResponse typedResponse = new PaymentAccountOwnerRegisteredEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.owner = (String) eventValues.getIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static PaymentAccountOwnerRegisteredEventResponse getPaymentAccountOwnerRegisteredEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PAYMENTACCOUNTOWNERREGISTERED_EVENT, log); + PaymentAccountOwnerRegisteredEventResponse typedResponse = new PaymentAccountOwnerRegisteredEventResponse(); + typedResponse.log = log; + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.owner = (String) eventValues.getIndexedValues().get(1).getValue(); + return typedResponse; + } + + @Deprecated + public static TangemPaymentAccountRegistry load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + return new TangemPaymentAccountRegistry(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + @Deprecated + public static TangemPaymentAccountRegistry load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + return new TangemPaymentAccountRegistry(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + public static TangemPaymentAccountRegistry load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + return new TangemPaymentAccountRegistry(contractAddress, web3j, credentials, contractGasProvider); + } + + public static TangemPaymentAccountRegistry load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return new TangemPaymentAccountRegistry(contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static RemoteCall deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider, String factory_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, factory_))); + return deployRemoteCall(TangemPaymentAccountRegistry.class, web3j, credentials, contractGasProvider, BINARY, encodedConstructor); + } + + public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider, String factory_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, factory_))); + return deployRemoteCall(TangemPaymentAccountRegistry.class, web3j, transactionManager, contractGasProvider, BINARY, encodedConstructor); + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit, String factory_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, factory_))); + return deployRemoteCall(TangemPaymentAccountRegistry.class, web3j, credentials, gasPrice, gasLimit, BINARY, encodedConstructor); + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit, String factory_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, factory_))); + return deployRemoteCall(TangemPaymentAccountRegistry.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, encodedConstructor); + } + + public Flowable paymentAccountCardChangedEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getPaymentAccountCardChangedEventFromLog(log)); + } + + public Flowable paymentAccountCardChangedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(PAYMENTACCOUNTCARDCHANGED_EVENT)); + return paymentAccountCardChangedEventFlowable(filter); + } + + public Flowable paymentAccountCardRegisteredEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getPaymentAccountCardRegisteredEventFromLog(log)); + } + + public Flowable paymentAccountCardRegisteredEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(PAYMENTACCOUNTCARDREGISTERED_EVENT)); + return paymentAccountCardRegisteredEventFlowable(filter); + } + + public Flowable paymentAccountOwnerChangedEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getPaymentAccountOwnerChangedEventFromLog(log)); + } + + public Flowable paymentAccountOwnerChangedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(PAYMENTACCOUNTOWNERCHANGED_EVENT)); + return paymentAccountOwnerChangedEventFlowable(filter); + } + + public Flowable paymentAccountOwnerRegisteredEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getPaymentAccountOwnerRegisteredEventFromLog(log)); + } + + public Flowable paymentAccountOwnerRegisteredEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(PAYMENTACCOUNTOWNERREGISTERED_EVENT)); + return paymentAccountOwnerRegisteredEventFlowable(filter); + } + + public RemoteFunctionCall changePaymentAccountCard(String previousCard, String newCard) { + final Function function = new Function( + FUNC_CHANGEPAYMENTACCOUNTCARD, + Arrays.asList(new Address(160, previousCard), + new Address(160, newCard)), + Collections.emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall changePaymentAccountOwner(String previousOwner, String newOwner) { + final Function function = new Function( + FUNC_CHANGEPAYMENTACCOUNTOWNER, + Arrays.asList(new Address(160, previousOwner), + new Address(160, newOwner)), + Collections.emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall factory() { + final Function function = new Function(FUNC_FACTORY, + List.of(), + List.of(new TypeReference
() { + })); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public RemoteFunctionCall initPaymentAccountCard(String card) { + final Function function = new Function( + FUNC_INITPAYMENTACCOUNTCARD, + List.of(new Address(160, card)), + Collections.emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall initPaymentAccountOwner(String paymentAccount, String owner) { + final Function function = new Function( + FUNC_INITPAYMENTACCOUNTOWNER, + Arrays.asList(new Address(160, paymentAccount), + new Address(160, owner)), + Collections.emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall paymentAccountByCard(String param0) { + final Function function = new Function(FUNC_PAYMENTACCOUNTBYCARD, + List.of(new Address(160, param0)), + List.of(new TypeReference
() { + })); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public RemoteFunctionCall paymentAccountsByOwner(String owner) { + final Function function = new Function(FUNC_PAYMENTACCOUNTSBYOWNER, + List.of(new Address(160, owner)), + List.of(new TypeReference>() { + })); + return new RemoteFunctionCall(function, + new Callable() { + @Override + @SuppressWarnings("unchecked") + public List call() throws Exception { + List result = (List) executeCallSingleValueReturn(function, List.class); + return convertToNative(result); + } + }); + } + + public static class PaymentAccountCardChangedEventResponse extends BaseEventResponse { + public String paymentAccount; + + public String previousCard; + + public String newCard; + } + + public static class PaymentAccountCardRegisteredEventResponse extends BaseEventResponse { + public String paymentAccount; + + public String card; + } + + public static class PaymentAccountOwnerChangedEventResponse extends BaseEventResponse { + public String paymentAccount; + + public String previousOwner; + + public String newOwner; + } + + public static class PaymentAccountOwnerRegisteredEventResponse extends BaseEventResponse { + public String paymentAccount; + + public String owner; + } +} diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt index c2ac96c707..21989a9ee6 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt @@ -7,7 +7,6 @@ import com.tangem.lib.visa.model.VisaBalancesAndLimits.Limits import com.tangem.lib.visa.utils.toBigDecimal import com.tangem.lib.visa.utils.toInstant import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import org.joda.time.Instant import org.web3j.protocol.Web3j import org.web3j.tx.TransactionManager @@ -18,39 +17,40 @@ internal class DefaultVisaContractInfoProvider( private val transactionManager: TransactionManager, private val gasProvider: ContractGasProvider, private val bridgeProcessorAddress: String, + private val paymentAccountRegistryAddress: String, private val dispatchers: CoroutineDispatcherProvider, ) : VisaContractInfoProvider { override suspend fun getBalancesAndLimits(walletAddress: String): VisaBalancesAndLimits { - return withContext(dispatchers.io) { - val tangemBridgeProcessor = TangemBridgeProcessor.load( - /* contractAddress = */ bridgeProcessorAddress, - /* web3j = */ web3j, - /* transactionManager = */ transactionManager, - /* contractGasProvider = */ gasProvider, - ) - - parZip( - dispatchers.io, - { loadPaymentAccount(walletAddress, tangemBridgeProcessor) }, - { loadPaymentTokenInfo(tangemBridgeProcessor) }, - { paymentAccount, paymentToken -> - fetchBalancesAndLimits(paymentAccount, paymentToken) - }, - ) - } + return parZip( + dispatchers.io, + { loadPaymentAccount(walletAddress) }, + { loadPaymentTokenInfo() }, + { paymentAccount, paymentToken -> + fetchBalancesAndLimits(paymentAccount, paymentToken) + }, + ) } - private fun loadPaymentAccount( - walletAddress: String, - tangemBridgeProcessor: TangemBridgeProcessor, - ): TangemPaymentAccount { - val paymentAccountAddress = tangemBridgeProcessor.getPaymentAccount(walletAddress).send() + private fun loadPaymentAccount(walletAddress: String): TangemPaymentAccount { + val paymentAccountRegistry = TangemPaymentAccountRegistry.load( + /* contractAddress = */ paymentAccountRegistryAddress, + /* web3j = */ web3j, + /* transactionManager = */ transactionManager, + /* contractGasProvider = */ gasProvider, + ) + val paymentAccountAddress = paymentAccountRegistry.paymentAccountByCard(walletAddress).send() return TangemPaymentAccount.load(paymentAccountAddress, web3j, transactionManager, gasProvider) } - private fun loadPaymentTokenInfo(tangemBridgeProcessor: TangemBridgeProcessor): PaymentTokenInfo { + private fun loadPaymentTokenInfo(): PaymentTokenInfo { + val tangemBridgeProcessor = TangemBridgeProcessor.load( + /* contractAddress = */ bridgeProcessorAddress, + /* web3j = */ web3j, + /* transactionManager = */ transactionManager, + /* contractGasProvider = */ gasProvider, + ) val paymentTokenContractAddress = tangemBridgeProcessor.paymentToken().send() val paymentTokenContract = ERC20.load(paymentTokenContractAddress, web3j, transactionManager, gasProvider) val paymentTokenDecimals = paymentTokenContract.decimals().send() diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt index 35a8a3c4ad..7829c34135 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt @@ -28,6 +28,7 @@ interface VisaContractInfoProvider { private val dispatchers: CoroutineDispatcherProvider, private val baseUrl: String = VisaConfig.BASE_RPC_URL, private val bridgeProcessorAddress: String = VisaConfig.BRIDGE_PROCESSOR_CONTRACT_ADDRESS, + private val paymentAccountRegistryAddress: String = VisaConfig.PAYMENT_ACCOUNT_REGISTRY_ADDRESS, private val chainId: Long = VisaConfig.CHAIN_ID, private val networkTimeoutSeconds: Long = VisaConfig.NETWORK_TIMEOUT_SECONDS, private val decimals: Int = VisaConfig.DECIMALS, @@ -45,6 +46,7 @@ interface VisaContractInfoProvider { transactionManager = transactionManager, gasProvider = gasProvider, bridgeProcessorAddress = bridgeProcessorAddress, + paymentAccountRegistryAddress = paymentAccountRegistryAddress, dispatchers = dispatchers, ) } diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt index 7819bd734e..e3d0a9dab5 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt @@ -2,8 +2,9 @@ package com.tangem.lib.visa.utils internal object VisaConfig { - const val BASE_RPC_URL = "https://rpc-mumbai.maticvigil.com/" - const val BRIDGE_PROCESSOR_CONTRACT_ADDRESS = "0x62119697e78178512bfcc456ae6d1b7dee9fbaa6" + const val BASE_RPC_URL = "https://polygon-mumbai.g.alchemy.com/v2/_1qqjXgBC_IikaXChnna8KTcV2eMMIQG/" + const val BRIDGE_PROCESSOR_CONTRACT_ADDRESS = "0xe32ecbbc1ec17fa9c160569cd613ad568ca50279" + const val PAYMENT_ACCOUNT_REGISTRY_ADDRESS = "0x3f4ae01073d1a9d5a92315fe118e57d1cdec7c44" const val CHAIN_ID = 80_001L const val DECIMALS = 9 const val GAS_LIMIT = 500_000_000L From 9ad7cd63ee5f5f4b65cd1f02be6cb355dd59b1c4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Mar 2024 13:24:50 +0000 Subject: [PATCH 038/109] Updated on 2026-08-14 --- .../tap/domain/tasks/product/ResetToFactorySettingsTask.kt | 2 +- .../com/tangem/tap/features/onboarding/OnboardingHelper.kt | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt index 96b0593c4e..09a32da708 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt @@ -33,7 +33,7 @@ class ResetToFactorySettingsTask( } private fun resetBackup(session: CardSession, callback: (result: CompletionResult) -> Unit) { - if (session.environment.card?.backupStatus?.isActive != true) { + if (session.environment.card?.backupStatus == Card.BackupStatus.NoBackup) { callback(CompletionResult.Success(session.environment.card!!)) return } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index dee9d0f733..f6b11bbe7b 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -9,6 +9,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.twinsIsTwinned +import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletBuilder @@ -41,8 +42,8 @@ object OnboardingHelper { response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> { val emptyWallets = response.card.wallets.isEmpty() val activationInProgress = onboardingManager?.isActivationInProgress(cardId) - val backupNotActive = response.card.backupStatus?.isActive != true - emptyWallets || activationInProgress == true || backupNotActive + val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup + emptyWallets || activationInProgress == true || isNoBackup } response.card.wallets.isNotEmpty() -> onboardingManager?.isActivationInProgress(cardId) ?: false From 35ea6dfe3c1b1293192b78d9ba2a03421afc1a55 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Mar 2024 19:10:52 +0500 Subject: [PATCH 039/109] Updated on 2026-08-14 --- .../tap/di/domain/SettingsDomainModule.kt | 4 +- domain/settings/build.gradle.kts | 1 + .../settings/IsSendTapHelpEnabledUseCase.kt | 3 +- .../settings/NeverShowTapHelpUseCase.kt | 16 ++++++ .../settings/NeverToShowTapHelpUseCase.kt | 13 ----- .../previewdata/AmountStatePreviewData.kt | 53 +++++++++++++++++++ .../state/previewdata/FeeStatePreviewData.kt | 52 ++++++++++++++++++ .../previewdata/RecipientStatePreviewData.kt | 28 ++++++++++ .../impl/presentation/ui/send/AmountBlock.kt | 45 +++++++++++++++- .../impl/presentation/ui/send/FeeBlock.kt | 40 +++++++++++++- .../presentation/ui/send/RecipientBlock.kt | 46 +++++++++++++++- .../presentation/viewmodel/SendViewModel.kt | 8 +-- 12 files changed, 286 insertions(+), 23 deletions(-) create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/NeverShowTapHelpUseCase.kt delete mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowTapHelpUseCase.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/RecipientStatePreviewData.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index ac3b9c779e..3c9fb7b2a9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -137,7 +137,7 @@ internal object SettingsDomainModule { @Provides @ViewModelScoped - fun provideNeverToShowTapHelpUseCase(settingsRepository: SettingsRepository): NeverToShowTapHelpUseCase { - return NeverToShowTapHelpUseCase(settingsRepository = settingsRepository) + fun provideNeverShowTapHelpUseCase(settingsRepository: SettingsRepository): NeverShowTapHelpUseCase { + return NeverShowTapHelpUseCase(settingsRepository = settingsRepository) } } \ No newline at end of file diff --git a/domain/settings/build.gradle.kts b/domain/settings/build.gradle.kts index 00cd0a439e..0c8b0de6e1 100644 --- a/domain/settings/build.gradle.kts +++ b/domain/settings/build.gradle.kts @@ -5,5 +5,6 @@ plugins { dependencies { implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) implementation(projects.domain.balanceHiding.models) } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IsSendTapHelpEnabledUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IsSendTapHelpEnabledUseCase.kt index 1e828a0bc6..cb59249538 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/IsSendTapHelpEnabledUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/IsSendTapHelpEnabledUseCase.kt @@ -1,5 +1,6 @@ package com.tangem.domain.settings +import arrow.core.Either import com.tangem.domain.settings.repositories.SettingsRepository /** @@ -9,5 +10,5 @@ import com.tangem.domain.settings.repositories.SettingsRepository */ class IsSendTapHelpEnabledUseCase(private val settingsRepository: SettingsRepository) { - suspend operator fun invoke(): Boolean = settingsRepository.isSendTapHelpPreviewEnabled() + suspend operator fun invoke() = Either.catch { settingsRepository.isSendTapHelpPreviewEnabled() } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/NeverShowTapHelpUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/NeverShowTapHelpUseCase.kt new file mode 100644 index 0000000000..603c9d7402 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/NeverShowTapHelpUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.SettingsRepository + +/** + * Never to show tap help + * + * @property settingsRepository settings repository + */ +class NeverShowTapHelpUseCase(private val settingsRepository: SettingsRepository) { + + suspend operator fun invoke() = Either.catch { + settingsRepository.setSendTapHelpPreviewAvailability(isEnabled = false) + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowTapHelpUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowTapHelpUseCase.kt deleted file mode 100644 index bf72970303..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowTapHelpUseCase.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.domain.settings - -import com.tangem.domain.settings.repositories.SettingsRepository - -/** - * Never to show tap help - * - * @property settingsRepository settings repository - */ -class NeverToShowTapHelpUseCase(private val settingsRepository: SettingsRepository) { - - suspend operator fun invoke() = settingsRepository.setSendTapHelpPreviewAvailability(isEnabled = false) -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt new file mode 100644 index 0000000000..8423ec20eb --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt @@ -0,0 +1,53 @@ +package com.tangem.features.send.impl.presentation.state.previewdata + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.model.Amount +import com.tangem.domain.tokens.model.AmountType +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.SendUiStateType +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal + +internal object AmountStatePreviewData { + + val amountState = SendStates.AmountState( + type = SendUiStateType.Amount, + isPrimaryButtonEnabled = false, + walletName = "Wallet", + walletBalance = stringReference("123.123"), + tokenIconState = TokenIconState.Loading, + segmentedButtonConfig = persistentListOf(), + amountTextField = SendTextField.AmountField( + value = "123.123123123123123123", + onValueChange = {}, + keyboardOptions = KeyboardOptions.Default, + keyboardActions = KeyboardActions.Default, + cryptoAmount = Amount( + currencySymbol = "ETH", + value = BigDecimal(123.123), + decimals = 18, + type = AmountType.CoinType, + ), + fiatAmount = Amount( + currencySymbol = "$", + value = BigDecimal(123.123), + decimals = 2, + type = AmountType.CoinType, + ), + isFiatValue = false, + fiatValue = "123.123", + isFiatUnavailable = false, + isError = false, + error = TextReference.EMPTY, + ), + ) + + val fiatAmountState = amountState.copy( + amountTextField = amountState.amountTextField.copy(isFiatValue = true), + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt new file mode 100644 index 0000000000..7cec508963 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt @@ -0,0 +1,52 @@ +package com.tangem.features.send.impl.presentation.state.previewdata + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType.Coin +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.SendUiStateType +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal + +internal object FeeStatePreviewData { + val feeState = SendStates.FeeState( + type = SendUiStateType.Amount, + isPrimaryButtonEnabled = false, + feeSelectorState = FeeSelectorState.Content( + fees = TransactionFee.Single( + normal = Fee.Common( + amount = Amount( + currencySymbol = "ETH", + value = BigDecimal(123.123), + decimals = 18, + type = Coin, + ), + ), + ), + selectedFee = FeeType.Market, + customValues = persistentListOf(), + ), + fee = Fee.Common( + amount = Amount( + currencySymbol = "ETH", + value = BigDecimal(123.123), + decimals = 18, + type = Coin, + ), + ), + rate = null, + appCurrency = AppCurrency( + code = "USD", + name = "USD", + symbol = "$", + iconSmallUrl = null, + iconMediumUrl = null, + ), + isFeeApproximate = false, + notifications = persistentListOf(), + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/RecipientStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/RecipientStatePreviewData.kt new file mode 100644 index 0000000000..f3008dc000 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/RecipientStatePreviewData.kt @@ -0,0 +1,28 @@ +package com.tangem.features.send.impl.presentation.state.previewdata + +import androidx.compose.foundation.text.KeyboardOptions +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import kotlinx.collections.immutable.persistentListOf + +internal object RecipientStatePreviewData { + + val recipientState = SendStates.RecipientState( + addressTextField = SendTextField.RecipientAddress( + value = "0x23948239805671983476598176", + onValueChange = {}, + keyboardOptions = KeyboardOptions.Default, + placeholder = stringReference("Placeholder"), + label = stringReference("Recipient"), + isError = false, + error = null, + ), + memoTextField = null, + recent = persistentListOf(), + wallets = persistentListOf(), + network = "Ethereum", + isValidating = false, + isPrimaryButtonEnabled = true, + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt index 1213f43797..26c0380dd5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt @@ -11,10 +11,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData @Composable internal fun AmountBlock( @@ -75,4 +79,43 @@ internal fun AmountBlock( private fun getAmountWithSymbol(amount: String, symbol: String): String { return "$amount $symbol" -} \ No newline at end of file +} + +// region Preview +@Preview +@Composable +private fun AmountBlockPreview_Light( + @PreviewParameter(AmountBlockPreviewProvider::class) value: SendStates.AmountState, +) { + TangemTheme { + AmountBlock( + amountState = value, + isSuccess = false, + isEditingDisabled = false, + onClick = {}, + ) + } +} + +@Preview +@Composable +private fun AmountBlockPreview_Dark( + @PreviewParameter(AmountBlockPreviewProvider::class) value: SendStates.AmountState, +) { + TangemTheme(isDark = true) { + AmountBlock( + amountState = value, + isSuccess = true, + isEditingDisabled = false, + onClick = {}, + ) + } +} + +private class AmountBlockPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + AmountStatePreviewData.amountState, + ) +} +// endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index 13ff8cab42..11c2b5d8de 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -15,6 +15,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.res.TangemTheme @@ -23,6 +26,7 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.state.previewdata.FeeStatePreviewData @Composable internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) { @@ -93,4 +97,38 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick ) } } -} \ No newline at end of file +} + +// region Preview +@Preview +@Composable +private fun FeeBlockPreview_Light(@PreviewParameter(FeeBlockPreviewProvider::class) value: SendStates.FeeState) { + TangemTheme { + FeeBlock( + feeState = value, + isSuccess = true, + onClick = {}, + ) + } +} + +@Preview +@Composable +private fun FeeBlockPreview_Dark(@PreviewParameter(FeeBlockPreviewProvider::class) value: SendStates.FeeState) { + TangemTheme(isDark = true) { + FeeBlock( + feeState = value, + isSuccess = true, + onClick = {}, + ) + } +} + +private class FeeBlockPreviewProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + FeeStatePreviewData.feeState, + ) +} +// endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt index 7169e38f49..473d043d35 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt @@ -10,11 +10,15 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.state.previewdata.RecipientStatePreviewData @Composable internal fun RecipientBlock( @@ -88,4 +92,44 @@ private fun MemoBlock(memo: SendTextField.RecipientMemo?) { modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), ) } -} \ No newline at end of file +} + +// region Preview +@Preview +@Composable +private fun RecipientBlockPreview_Light( + @PreviewParameter(RecipientBlockPreviewProvider::class) value: SendStates.RecipientState, +) { + TangemTheme { + RecipientBlock( + recipientState = value, + isSuccess = true, + isEditingDisabled = false, + onClick = {}, + ) + } +} + +@Preview +@Composable +private fun RecipientBlockPreview_Dark( + @PreviewParameter(RecipientBlockPreviewProvider::class) value: SendStates.RecipientState, +) { + TangemTheme(isDark = true) { + RecipientBlock( + recipientState = value, + isSuccess = true, + isEditingDisabled = false, + onClick = {}, + ) + } +} + +private class RecipientBlockPreviewProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + RecipientStatePreviewData.recipientState, + ) +} +// endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 588ea6b0a2..bc74ced905 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -17,7 +17,7 @@ import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase -import com.tangem.domain.settings.NeverToShowTapHelpUseCase +import com.tangem.domain.settings.NeverShowTapHelpUseCase import com.tangem.domain.tokens.* import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency @@ -88,7 +88,7 @@ internal class SendViewModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val parseQrCodeUseCase: ParseQrCodeUseCase, private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase, - private val neverToShowTapHelpUseCase: NeverToShowTapHelpUseCase, + private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, currencyChecksRepository: CurrencyChecksRepository, isFeeApproximateUseCase: IsFeeApproximateUseCase, getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, @@ -295,7 +295,7 @@ internal class SendViewModel @Inject constructor( private fun getTapHelpPreviewAvailability() { viewModelScope.launch(dispatchers.main) { - isTapHelpPreviewEnabled = isSendTapHelpEnabledUseCase() + isTapHelpPreviewEnabled = isSendTapHelpEnabledUseCase().getOrElse { false } } } @@ -831,7 +831,7 @@ internal class SendViewModel @Inject constructor( private fun setNeverToShowTapHelp() { viewModelScope.launch(dispatchers.main) { - neverToShowTapHelpUseCase() + neverShowTapHelpUseCase() } uiState = stateFactory.getHiddenTapHelpState() } From a6ab6a6eaf0a22d048848bdf70d21a00210a42c7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Mar 2024 12:43:32 +0100 Subject: [PATCH 040/109] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 7 ++- .../src/main/res/values-zh-rTW/strings.xml | 2 - core/res/src/main/res/values/strings.xml | 9 ++- .../swap/converters/ErrorsDataConverter.kt | 2 + .../feature/swap/domain/models/DataError.kt | 4 ++ .../domain/models/ui/SwapTransactionState.kt | 29 ++++++++++ .../feature/swap/domain/models/ui/TxState.kt | 22 -------- .../feature/swap/domain/SwapInteractor.kt | 6 +- .../feature/swap/domain/SwapInteractorImpl.kt | 55 ++++++++++--------- .../tangem/feature/swap/ui/StateBuilder.kt | 51 ++++++++++++++--- .../tangem/feature/swap/ui/TransactionCard.kt | 6 +- .../feature/swap/viewmodels/SwapViewModel.kt | 10 ++-- .../components/exchange/ExchangeProvider.kt | 3 +- 13 files changed, 131 insertions(+), 75 deletions(-) create mode 100644 features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt delete mode 100644 features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 79539f13a8..95cd7e9498 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -183,6 +183,9 @@ Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами Выберите провайдера Произошла ошибка. Код: %s + К сожалению обмен указанной пары через выбранного провайдера на данный момент не возможен. Попробуйте совершить обмен позже. (Код:\u00A0%s) + Выбранный провайдер не доступен для обмена. Попробуйте позже. (Код:\u00A0%s) + В данный момент обмен не возможен. Попробуйте позже. (Код:\u00A0%s) Курс обмена Обмен через %s Чтобы вернуть ваши деньги, посетите сайт провайдера @@ -273,10 +276,11 @@ Защита Позже вы сможете установить индивидуальный код доступа для каждой карты Персонализация - Код доступа можно восстановить с помощью привязанной карты. Не храните все карты в одном месте + Код доступа можно восстановить с помощью привязанной карты. Не храните все карты в одном месте. Восстановление Выберите любое слово, фразу или число в качестве кода доступа Создайте код доступа + Введите код доступа еще раз, чтобы избежать ошибки Повторно введите код доступа Код доступа должен состоять не менее чем из 4 символов. Введенные коды доступа не совпадают @@ -619,6 +623,7 @@ Нет доступных для обмена токенов Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s Невозможно покрыть комиссию %s + Сумма получения не может быть менее %s Cервис временно недоступен Сумма для обмена должна быть не более %s Сумма для обмена должна быть не менее %s diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index a8a013bebc..841f0df3bb 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -139,11 +139,9 @@ 要訪問所有的網路您需要掃描卡片 掃描卡片 代幣 - 您必須設置一個單一的訪問代碼來保護您的所有錢包 保護 您可以稍後在每張卡上設置單獨的訪問密碼 個人化 - 門禁密碼可以用聯動卡恢復,不要把所有卡都放在一個地方 恢復 選擇您想要的任何單詞、短語或數字作為您的訪問代碼 建立訪問密碼 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index d91eeb01d1..597b184e40 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -182,6 +182,9 @@ Tangem offers token swaps via 3rd-party providers according to each provider\'s terms Choose provider An error occurred. Code: %s + Oops! Swapping the selected pair through the chosen provider is temporarily unavailable. Please try again later. (Code:\u00A0%s) + Selected provider is unavailable at the moment. Please try again later. (Code:\u00A0%s) + Swaps are unavailable at the moment. Please try again later. (Code:\u00A0%s) Estimated amount Exchange by %s Visit provider’s website to refund your money @@ -269,14 +272,15 @@ Upvote Choose wallet The wallet doesn\'t support more than one network - You have to set up a single access code to protect all your wallets + You have to set up a single access code to protect all your cards Protect You can set up an individual access code on each card later Personalize - The access code can be restored with a linked card, don\'t keep all cards in one place + The access code can be restored with a linked card. Don’t keep all cards at one place. Restore Choose any word, phrase, or number you want as your access code Create Access Code + Enter your access code one more time to avoid a mistake Re-enter your Access Code Access code must be at least 4 characters long Entered access code didn\'t match the initial access code @@ -629,6 +633,7 @@ No available tokens to swap To make a transaction you need to deposit some %1$s %2$s Unable to cover %s fee + The amount to receive must be at least %s Service temporarily unavailable The amount of tokens to be swapped must not exceed %s The amount to swap must be at least %s diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt index dca890c55e..ee0cf5d41d 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt @@ -18,9 +18,11 @@ internal class ErrorsDataConverter( return when (error.code) { 2010 -> DataError.BadRequest(code = error.code) + 2200 -> DataError.SwapsAreUnavailableNowError(code = error.code) 2210 -> DataError.ExchangeProviderNotFoundError(code = error.code) 2220 -> DataError.ExchangeProviderNotActiveError(code = error.code) 2230 -> DataError.ExchangeProviderNotAvailableError(code = error.code) + 2231 -> DataError.ExchangeProviderProviderInternalError(code = error.code) 2240 -> DataError.ExchangeNotPossibleError(code = error.code) 2250 -> tryParseExchangeTooSmallAmountError(error = error) 2251 -> tryParseExchangeTooBigAmountError(error = error) diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt index d4881c27b8..12e5db5e37 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt @@ -8,12 +8,16 @@ sealed class DataError { data class BadRequest(override val code: Int) : DataError() + data class SwapsAreUnavailableNowError(override val code: Int) : DataError() + data class ExchangeProviderNotFoundError(override val code: Int) : DataError() data class ExchangeProviderNotActiveError(override val code: Int) : DataError() data class ExchangeProviderNotAvailableError(override val code: Int) : DataError() + data class ExchangeProviderProviderInternalError(override val code: Int) : DataError() + data class ExchangeNotPossibleError(override val code: Int) : DataError() data class ExchangeTooSmallAmountError(override val code: Int, val amount: SwapAmount) : DataError() diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt new file mode 100644 index 0000000000..6ba68ddd31 --- /dev/null +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.swap.domain.models.ui + +import com.tangem.feature.swap.domain.models.DataError +import java.math.BigDecimal + +sealed class SwapTransactionState { + + data class TxSent( + val fromAmount: String? = null, + val fromAmountValue: BigDecimal? = null, + val toAmount: String? = null, + val toAmountValue: BigDecimal? = null, + val txHash: String, + val txExternalUrl: String? = null, + val timestamp: Long, + ) : SwapTransactionState() + + data object UserCancelled : SwapTransactionState() + + data object BlockchainError : SwapTransactionState() + + data object TangemSdkError : SwapTransactionState() + + data object NetworkError : SwapTransactionState() + + data object UnknownError : SwapTransactionState() + + data class ExpressError(val dataError: DataError) : SwapTransactionState() +} \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt deleted file mode 100644 index 14259bc672..0000000000 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.feature.swap.domain.models.ui - -import java.math.BigDecimal - -sealed class TxState { - - data class TxSent( - val fromAmount: String? = null, - val fromAmountValue: BigDecimal? = null, - val toAmount: String? = null, - val toAmountValue: BigDecimal? = null, - val txHash: String, - val txExternalUrl: String? = null, - val timestamp: Long, - ) : TxState() - - object UserCancelled : TxState() - object BlockchainError : TxState() - object TangemSdkError : TxState() - object NetworkError : TxState() - object UnknownError : TxState() -} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index b43322b46a..dafaff9a51 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -21,7 +21,7 @@ interface SwapInteractor { * @param permissionOptions data to give permissions */ @Throws(IllegalStateException::class) - suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState + suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): SwapTransactionState /** * Find best quote for given tokens to swap @@ -51,7 +51,7 @@ interface SwapInteractor { * @param currencyToGet [Currency] * @param amountToSwap amount to swap * @param fee for tx - * @return [TxState] + * @return [SwapTransactionState] */ @Suppress("LongParameterList") @Throws(IllegalStateException::class) @@ -63,7 +63,7 @@ interface SwapInteractor { amountToSwap: String, includeFeeInAmount: IncludeFeeInAmount, fee: TxFee, - ): TxState + ): SwapTransactionState suspend fun updateQuotesStateWithSelectedFee( state: SwapState.QuotesLoadedState, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index b401d09f3c..6df1eadf9d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -197,7 +197,10 @@ internal class SwapInteractorImpl @Inject constructor( return repository.getPairs(initialCurrency, currenciesList) } - override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState { + override suspend fun givePermissionToSwap( + networkId: String, + permissionOptions: PermissionOptions, + ): SwapTransactionState { val derivationPath = permissionOptions.fromToken.network.derivationPath.value val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) { getApproveData( @@ -227,16 +230,16 @@ internal class SwapInteractorImpl @Inject constructor( return when (result) { is SendTxResult.Success -> { allowPermissionsHandler.addAddressToInProgress(permissionOptions.forTokenContractAddress) - TxState.TxSent( + SwapTransactionState.TxSent( txHash = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), timestamp = System.currentTimeMillis(), ) } - SendTxResult.UserCancelledError -> TxState.UserCancelled - is SendTxResult.BlockchainSdkError -> TxState.BlockchainError - is SendTxResult.TangemSdkError -> TxState.TangemSdkError - is SendTxResult.NetworkError -> TxState.NetworkError - is SendTxResult.UnknownError -> TxState.UnknownError + SendTxResult.UserCancelledError -> SwapTransactionState.UserCancelled + is SendTxResult.BlockchainSdkError -> SwapTransactionState.BlockchainError + is SendTxResult.TangemSdkError -> SwapTransactionState.TangemSdkError + is SendTxResult.NetworkError -> SwapTransactionState.NetworkError + is SendTxResult.UnknownError -> SwapTransactionState.UnknownError } } @@ -442,7 +445,7 @@ internal class SwapInteractorImpl @Inject constructor( amountToSwap: String, includeFeeInAmount: IncludeFeeInAmount, fee: TxFee, - ): TxState { + ): SwapTransactionState { return when (swapProvider.type) { ExchangeProviderType.CEX -> { val amountDecimal = toBigDecimalOrNull(amountToSwap) @@ -519,7 +522,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGet: CryptoCurrency, amountToSwap: String, fee: TxFee, - ): TxState { + ): SwapTransactionState { val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } val amount = SwapAmount(amountDecimal, currencyToSend.decimals) val derivationPath = currencyToSend.network.derivationPath.value @@ -543,7 +546,7 @@ internal class SwapInteractorImpl @Inject constructor( return when (result) { is SendTxResult.Success -> { storeLastCryptoCurrencyId(currencyToGet) - TxState.TxSent( + SwapTransactionState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, currencyToSend.symbol, @@ -558,11 +561,11 @@ internal class SwapInteractorImpl @Inject constructor( timestamp = System.currentTimeMillis(), ) } - SendTxResult.UserCancelledError -> TxState.UserCancelled - is SendTxResult.BlockchainSdkError -> TxState.BlockchainError - is SendTxResult.TangemSdkError -> TxState.TangemSdkError - is SendTxResult.NetworkError -> TxState.NetworkError - is SendTxResult.UnknownError -> TxState.UnknownError + SendTxResult.UserCancelledError -> SwapTransactionState.UserCancelled + is SendTxResult.BlockchainSdkError -> SwapTransactionState.BlockchainError + is SendTxResult.TangemSdkError -> SwapTransactionState.TangemSdkError + is SendTxResult.NetworkError -> SwapTransactionState.NetworkError + is SendTxResult.UnknownError -> SwapTransactionState.UnknownError } } @@ -574,7 +577,7 @@ internal class SwapInteractorImpl @Inject constructor( txFee: TxFee, swapProvider: SwapProvider, userWalletId: UserWalletId, - ): TxState { + ): SwapTransactionState { val exchangeData = repository.getExchangeData( fromContractAddress = currencyToSend.currency.getContractAddress(), fromNetwork = currencyToSend.currency.network.backendId, @@ -586,15 +589,17 @@ internal class SwapInteractorImpl @Inject constructor( providerId = swapProvider.providerId, rateType = RateType.FLOAT, toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value ?: "", - ).getOrNull() + ).getOrElse { return SwapTransactionState.ExpressError(it) } + + val exchangeDataCex = + exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.UnknownError - val exchangeDataCex = exchangeData?.transaction as? ExpressTransactionModel.CEX ?: return TxState.UnknownError val txExtras = transactionManager.getMemoExtras( currencyToSend.currency.network.backendId, exchangeDataCex.txExtraId, ) if (txExtras == null && exchangeDataCex.txExtraId != null) { - return TxState.UnknownError + return SwapTransactionState.UnknownError } val txData = walletManagersFacade.createTransaction( amount = amount.value.convertToAmount(currencyToSend.currency), @@ -622,11 +627,11 @@ internal class SwapInteractorImpl @Inject constructor( return result.fold( ifLeft = { when (it) { - SendTransactionError.UserCancelledError -> TxState.UserCancelled - is SendTransactionError.BlockchainSdkError -> TxState.BlockchainError - is SendTransactionError.TangemSdkError -> TxState.TangemSdkError - is SendTransactionError.NetworkError -> TxState.NetworkError - else -> TxState.UnknownError + SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled + is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError + is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError + is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError + else -> SwapTransactionState.UnknownError } }, ifRight = { @@ -643,7 +648,7 @@ internal class SwapInteractorImpl @Inject constructor( txExternalId = txCexModel?.externalTxId.orEmpty(), ) storeLastCryptoCurrencyId(currencyToGet.currency) - TxState.TxSent( + SwapTransactionState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, currencyToSend.currency.symbol, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index fc73c5029a..70bfd13e22 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -926,7 +926,7 @@ internal class StateBuilder( @Suppress("LongParameterList") fun createSuccessState( uiState: SwapStateHolder, - txState: TxState.TxSent, + swapTransactionState: SwapTransactionState.TxSent, dataState: SwapProcessDataState, onExploreClick: () -> Unit, onStatusClick: () -> Unit, @@ -935,8 +935,8 @@ internal class StateBuilder( val fee = requireNotNull(dataState.selectedFee) val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency) val toCryptoCurrency = requireNotNull(dataState.toCryptoCurrency) - val fromAmount = txState.fromAmountValue ?: BigDecimal.ZERO - val toAmount = txState.toAmountValue ?: BigDecimal.ZERO + val fromAmount = swapTransactionState.fromAmountValue ?: BigDecimal.ZERO + val toAmount = swapTransactionState.toAmountValue ?: BigDecimal.ZERO val providerState = uiState.providerState as ProviderState.Content val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount)) @@ -944,7 +944,7 @@ internal class StateBuilder( return uiState.copy( successState = SwapSuccessStateHolder( - timestamp = txState.timestamp, + timestamp = swapTransactionState.timestamp, txUrl = txUrl, providerName = stringReference(providerState.name), providerType = stringReference(providerState.type), @@ -952,8 +952,8 @@ internal class StateBuilder( providerIcon = providerState.iconUrl, rate = providerState.subtitle, fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"), - fromTokenAmount = stringReference(txState.fromAmount.orEmpty()), - toTokenAmount = stringReference(txState.toAmount.orEmpty()), + fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()), + toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()), fromTokenFiatAmount = stringReference(fromFiatAmount), toTokenFiatAmount = stringReference(toFiatAmount), fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency), @@ -964,17 +964,50 @@ internal class StateBuilder( ) } - fun createErrorTransaction(uiState: SwapStateHolder, txState: TxState, onAlertClick: () -> Unit): SwapStateHolder { + fun createErrorTransaction( + uiState: SwapStateHolder, + swapTransactionState: SwapTransactionState, + onAlertClick: () -> Unit, + ): SwapStateHolder { return uiState.copy( alert = SwapWarning.GenericWarning( - message = null, + message = if (swapTransactionState is SwapTransactionState.ExpressError) { + getAlertErrorMessage(swapTransactionState.dataError) + } else { + null + }, onClick = onAlertClick, - type = if (txState is TxState.NetworkError) GenericWarningType.NETWORK else GenericWarningType.OTHER, + type = if (swapTransactionState is SwapTransactionState.NetworkError) { + GenericWarningType.NETWORK + } else { + GenericWarningType.OTHER + }, ), changeCardsButtonState = ChangeCardsButtonState.ENABLED, ) } + private fun getAlertErrorMessage(dataError: DataError): TextReference? { + return when (dataError) { + is DataError.SwapsAreUnavailableNowError -> resourceReference( + id = R.string.express_error_swap_unavailable, + formatArgs = wrappedList(dataError.code), + ) + is DataError.ExchangeNotPossibleError -> resourceReference( + id = R.string.express_error_provider_unavailable, + formatArgs = wrappedList(dataError.code), + ) + is DataError.ExchangeProviderNotActiveError, + is DataError.ExchangeProviderNotAvailableError, + is DataError.ExchangeProviderProviderInternalError, + -> resourceReference( + id = R.string.express_error_swap_pair_unavailable, + formatArgs = wrappedList(dataError.code), + ) + else -> null + } + } + fun createAlert( uiState: SwapStateHolder, isPriceImpact: Boolean, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index d9a604c6d4..bd77a3f66e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -283,7 +283,7 @@ private fun Content( if (amountEquivalent != null) { if (type is TransactionCardType.ReadOnly) { - Row { + Row(modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20)) { if (priceImpact is PriceImpact.Value) { Text( text = makePriceImpactBalanceWarning( @@ -293,7 +293,6 @@ private fun Content( color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size20) .align(Alignment.CenterVertically), ) } else { @@ -302,7 +301,6 @@ private fun Content( text = it, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, - modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), ) } } @@ -564,7 +562,7 @@ private fun TransactionCardPreview() { @Suppress("MagicNumber") private fun TransactionCardPreviewWithPriceImpact() { TransactionCard( - type = TransactionCardType.ReadOnly(), + type = TransactionCardType.ReadOnly(showWarning = true), amountEquivalent = "1 000 000", tokenIconUrl = "", tokenCurrency = "DAI", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 90e9ca9b8c..b297169695 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -503,7 +503,7 @@ internal class SwapViewModel @Inject constructor( ) }.onSuccess { when (it) { - is TxState.TxSent -> { + is SwapTransactionState.TxSent -> { sendSuccessSwapEvent(fromCurrency.currency, fee.feeType) val url = blockchainInteractor.getExplorerTransactionLink( networkId = fromCurrency.currency.network.backendId, @@ -512,7 +512,7 @@ internal class SwapViewModel @Inject constructor( updateWalletBalance() uiState = stateBuilder.createSuccessState( uiState = uiState, - txState = it, + swapTransactionState = it, dataState = dataState, txUrl = url, onExploreClick = { @@ -537,7 +537,7 @@ internal class SwapViewModel @Inject constructor( swapRouter.openScreen(SwapNavScreen.Success) } - is TxState.UserCancelled -> { + is SwapTransactionState.UserCancelled -> { startLoadingQuotesFromLastState() } else -> { @@ -604,14 +604,14 @@ internal class SwapViewModel @Inject constructor( ) }.onSuccess { when (it) { - is TxState.TxSent -> { + is SwapTransactionState.TxSent -> { sendApproveSuccessEvent(fromToken, feeForPermission.feeType, approveType) updateWalletBalance() uiState = stateBuilder.loadingPermissionState(uiState) uiState = stateBuilder.dismissBottomSheet(uiState) startLoadingQuotesFromLastState(isSilent = true) } - is TxState.UserCancelled -> Unit + is SwapTransactionState.UserCancelled -> Unit else -> { uiState = stateBuilder.createErrorTransaction(uiState, it) { uiState = stateBuilder.clearAlert(uiState) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt index 41278b195c..173e79dc30 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.inputrow.InputRowBestRate import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme @Composable @@ -68,7 +67,7 @@ internal fun ExchangeProvider( ) Text( modifier = Modifier.align(Alignment.CenterVertically), - text = stringResource(R.string.express_transaction_id, wrappedList(providerTxId)), + text = stringResource(R.string.express_transaction_id, providerTxId), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) From cdce04bb6ff3d460d107b3b59ecb6c7922175af3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Mar 2024 17:50:33 +0300 Subject: [PATCH 041/109] Updated on 2026-08-14 --- .../features/details/redux/DetailsAction.kt | 36 ++++++++++--------- .../features/details/redux/DetailsReducer.kt | 8 +++++ .../features/details/redux/DetailsState.kt | 1 + .../details/ui/resetcard/ResetCardScreen.kt | 31 ++++++++++++++-- .../ui/resetcard/ResetCardScreenState.kt | 7 ++++ .../ui/resetcard/ResetCardViewModel.kt | 20 ++++++++++- 6 files changed, 82 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 627b9bf0d5..05c586372d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -16,29 +16,31 @@ sealed class DetailsAction : Action { val shouldSaveUserWallets: Boolean, ) : DetailsAction() - object ReCreateTwinsWallet : DetailsAction() + data object ReCreateTwinsWallet : DetailsAction() sealed class ResetToFactory : DetailsAction() { - object Start : ResetToFactory() - object Proceed : ResetToFactory() + data object Start : ResetToFactory() + data object Proceed : ResetToFactory() data class AcceptCondition1(val accepted: Boolean) : ResetToFactory() data class AcceptCondition2(val accepted: Boolean) : ResetToFactory() - object Failure : ResetToFactory() - object Success : ResetToFactory() + data object Failure : ResetToFactory() + data object Success : ResetToFactory() + + data class LastWarningDialogVisibility(val isShown: Boolean) : ResetToFactory() } - object ScanCard : DetailsAction() + data object ScanCard : DetailsAction() data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction() - object ResetCardSettingsData : DetailsAction() - object ScanAndSaveUserWallet : DetailsAction() { + data object ResetCardSettingsData : DetailsAction() + data object ScanAndSaveUserWallet : DetailsAction() { - object Success : DetailsAction() + data object Success : DetailsAction() data class Error(val error: TextReference?) : DetailsAction() } - object DismissError : DetailsAction() + data object DismissError : DetailsAction() sealed class AccessCodeRecovery : DetailsAction() { object Open : AccessCodeRecovery() @@ -50,14 +52,14 @@ sealed class DetailsAction : Action { } sealed class ManageSecurity : DetailsAction() { - object OpenSecurity : ManageSecurity() + data object OpenSecurity : ManageSecurity() data class SelectOption(val option: SecurityOption) : ManageSecurity() - object SaveChanges : ManageSecurity() { - object Success : ManageSecurity() - object Failure : ManageSecurity() + data object SaveChanges : ManageSecurity() { + data object Success : ManageSecurity() + data object Failure : ManageSecurity() } - object ChangeAccessCode : ManageSecurity() + data object ChangeAccessCode : ManageSecurity() } sealed class AppSettings : DetailsAction() { @@ -65,7 +67,7 @@ sealed class DetailsAction : Action { val enable: Boolean, val setting: AppSetting, ) : AppSettings() { - object Success : AppSettings() + data object Success : AppSettings() data class Failure( val prevState: Boolean, @@ -77,7 +79,7 @@ sealed class DetailsAction : Action { val lifecycleScope: LifecycleCoroutineScope, ) : AppSettings() - object EnrollBiometrics : AppSettings() + data object EnrollBiometrics : AppSettings() data class BiometricsStatusChanged( val needEnrollBiometrics: Boolean, ) : AppSettings() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index e829d9c937..5b3f2fafd3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -114,6 +114,7 @@ private fun handlePrepareCardSettingsScreen( null }, isShowPasswordResetRadioButton = isShowPasswordResetRadioButton, + isLastWarningDialogShown = false, ) return state.copy(cardSettingsState = cardSettingsState) } @@ -181,6 +182,13 @@ private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: Detai ), ) } + is DetailsAction.ResetToFactory.LastWarningDialogVisibility -> { + state.copy( + cardSettingsState = cardSettingsState?.copy( + isLastWarningDialogShown = action.isShown, + ), + ) + } else -> state } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 1eab7ec856..e1c523e759 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -46,6 +46,7 @@ data class CardSettingsState( val condition2Checked: Boolean, val accessCodeRecovery: AccessCodeRecoveryState? = null, val isShowPasswordResetRadioButton: Boolean, + val isLastWarningDialogShown: Boolean, ) data class ManageSecurityState( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index 1320abaed7..35b4c9bae8 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -11,9 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerH12 -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.details.ui.cardsettings.resolveReference @@ -35,6 +33,8 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni }, onBackClick = onBackClick, ) + + LastWarningDialog(state = state) } @Composable @@ -181,6 +181,26 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) { ) } +@Composable +private fun LastWarningDialog(state: ResetCardScreenState) { + if (state is ResetCardScreenState.ResetCardScreenContent && state.lastWarningDialog.isShown) { + BasicDialog( + title = stringResource(id = R.string.common_attention), + message = stringResource(id = R.string.card_settings_action_sheet_title), + dismissButton = DialogButton( + title = stringResource(id = R.string.card_settings_action_sheet_reset), + warning = true, + onClick = state.lastWarningDialog.onResetButtonClick, + ), + confirmButton = DialogButton( + title = stringResource(id = R.string.common_cancel), + onClick = state.lastWarningDialog.onDismiss, + ), + onDismissDialog = state.lastWarningDialog.onDismiss, + ) + } +} + // region Preview @Composable private fun ResetCardScreenSample(modifier: Modifier = Modifier) { @@ -196,6 +216,11 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) { onAcceptCondition1ToggleClick = {}, onAcceptCondition2ToggleClick = {}, onResetButtonClick = {}, + lastWarningDialog = ResetCardScreenState.ResetCardScreenContent.LastWarningDialog( + isShown = false, + onResetButtonClick = {}, + onDismiss = {}, + ), ), onBackClick = {}, ) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt index a12decbeef..d70071ed49 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt @@ -15,9 +15,16 @@ internal sealed class ResetCardScreenState { val onAcceptCondition1ToggleClick: (Boolean) -> Unit, val onAcceptCondition2ToggleClick: (Boolean) -> Unit, val onResetButtonClick: () -> Unit, + val lastWarningDialog: LastWarningDialog, ) : ResetCardScreenState() { val resetButtonEnabled: Boolean get() = accepted + + data class LastWarningDialog( + val isShown: Boolean, + val onResetButtonClick: () -> Unit, + val onDismiss: () -> Unit, + ) } internal enum class WarningsToReset { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index 62218c3752..02e00914e1 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -30,7 +30,25 @@ internal class ResetCardViewModel(private val store: Store) { acceptCondition2Checked = state?.condition2Checked ?: false, onAcceptCondition1ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition1(it)) }, onAcceptCondition2ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition2(it)) }, - onResetButtonClick = { store.dispatch(DetailsAction.ResetToFactory.Proceed) }, + onResetButtonClick = { showLastWarningDialog() }, + lastWarningDialog = ResetCardScreenState.ResetCardScreenContent.LastWarningDialog( + isShown = state?.isLastWarningDialogShown ?: false, + onResetButtonClick = ::onLastWarningDialogResetClicked, + onDismiss = ::onLastWarningDialogDismiss, + ), ) } + + private fun showLastWarningDialog() { + store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = true)) + } + + private fun onLastWarningDialogResetClicked() { + store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = false)) + store.dispatch(DetailsAction.ResetToFactory.Proceed) + } + + private fun onLastWarningDialogDismiss() { + store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = false)) + } } \ No newline at end of file From c71d0e27a81216bd5d69614c7ab963cca43556ab Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Mar 2024 15:46:10 +0100 Subject: [PATCH 042/109] Updated on 2026-08-14 --- .../core/ui/extensions/BlockchainIcons.kt | 20 ++++++ core/ui/src/main/res/drawable/ic_flare_22.xml | 10 +++ .../ui/src/main/res/drawable/ic_mantle_22.xml | 54 ++++++++++++++++ .../src/main/res/drawable/ic_moonriver_22.xml | 36 +++++++++++ .../ui/src/main/res/drawable/ic_taraxa_22.xml | 10 +++ .../ui/src/main/res/drawable/img_flare_22.xml | 17 ++++++ .../src/main/res/drawable/img_mantle_22.xml | 61 +++++++++++++++++++ .../main/res/drawable/img_moonriver_22.xml | 43 +++++++++++++ .../src/main/res/drawable/img_taraxa_22.xml | 17 ++++++ .../domain/common/extensions/Blockchain.kt | 20 ++++++ gradle/dependencies.toml | 2 +- 11 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 core/ui/src/main/res/drawable/ic_flare_22.xml create mode 100644 core/ui/src/main/res/drawable/ic_mantle_22.xml create mode 100644 core/ui/src/main/res/drawable/ic_moonriver_22.xml create mode 100644 core/ui/src/main/res/drawable/ic_taraxa_22.xml create mode 100644 core/ui/src/main/res/drawable/img_flare_22.xml create mode 100644 core/ui/src/main/res/drawable/img_mantle_22.xml create mode 100644 core/ui/src/main/res/drawable/img_moonriver_22.xml create mode 100644 core/ui/src/main/res/drawable/img_taraxa_22.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index d7b3fd7f1d..0a77e69e7e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -61,6 +61,10 @@ fun getActiveIconRes(blockchainId: String): Int { "moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22 "manta", "manta/test" -> R.drawable.img_manta_22 "polygonZkEVM", "polygonZkEVM/test" -> R.drawable.img_polygon_22 + "moonriver", "moonriver/test" -> R.drawable.img_moonriver_22 + "mantle", "mantle/test" -> R.drawable.img_mantle_22 + "flare", "flare/test" -> R.drawable.img_flare_22 + "taraxa", "taraxa/test" -> R.drawable.img_taraxa_22 "radiant" -> R.drawable.img_radiant_22 else -> R.drawable.ic_alert_24 } @@ -124,6 +128,10 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22 "manta-network", "manta-network/test" -> R.drawable.img_manta_22 "polygon-zkevm", "polygon-zkevm/test" -> R.drawable.img_polygon_22 + "moonriver", "moonriver/test" -> R.drawable.img_moonriver_22 + "mantle", "mantle/test" -> R.drawable.img_mantle_22 + "flare-network", "flare-network/test" -> R.drawable.img_flare_22 + "taraxa", "taraxa/test" -> R.drawable.img_taraxa_22 "radiant" -> R.drawable.img_radiant_22 else -> R.drawable.ic_alert_24 } @@ -184,6 +192,10 @@ fun getActiveIconResByCoinId(coinId: String): Int { "moonbeam" -> R.drawable.img_moonbeam_22 "manta-network-ethereum" -> R.drawable.img_manta_22 "polygon-zkevm-ethereum" -> R.drawable.img_polygon_22 + "moonriver" -> R.drawable.img_moonriver_22 + "mantle" -> R.drawable.img_mantle_22 + "flare-networks" -> R.drawable.img_flare_22 + "taraxa" -> R.drawable.img_taraxa_22 "radiant" -> R.drawable.img_radiant_22 else -> R.drawable.ic_alert_24 } @@ -247,6 +259,10 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22 "manta", "manta/test" -> R.drawable.ic_manta_22 "polygonZkEVM", "polygonZkEVM/test" -> R.drawable.ic_polygon_22 + "moonriver", "moonriver/test" -> R.drawable.ic_moonriver_22 + "mantle", "mantle/test" -> R.drawable.ic_mantle_22 + "flare", "flare/test" -> R.drawable.ic_flare_22 + "taraxa", "taraxa/test" -> R.drawable.ic_taraxa_22 "radiant" -> R.drawable.ic_radiant_22 else -> R.drawable.ic_alert_24 } @@ -310,6 +326,10 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22 "manta-network", "manta-network/test" -> R.drawable.ic_manta_22 "polygon-zkevm", "polygon-zkevm/test" -> R.drawable.ic_polygon_22 + "moonriver", "moonriver/test" -> R.drawable.ic_moonriver_22 + "mantle", "mantle/test" -> R.drawable.ic_mantle_22 + "flare-network", "flare-network/test" -> R.drawable.ic_flare_22 + "taraxa", "taraxa/test" -> R.drawable.ic_taraxa_22 "radiant" -> R.drawable.ic_radiant_22 else -> R.drawable.ic_alert_24 } diff --git a/core/ui/src/main/res/drawable/ic_flare_22.xml b/core/ui/src/main/res/drawable/ic_flare_22.xml new file mode 100644 index 0000000000..96c5c7eea9 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_flare_22.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_mantle_22.xml b/core/ui/src/main/res/drawable/ic_mantle_22.xml new file mode 100644 index 0000000000..b53a906acb --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_mantle_22.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_moonriver_22.xml b/core/ui/src/main/res/drawable/ic_moonriver_22.xml new file mode 100644 index 0000000000..0a44cf4e25 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_moonriver_22.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_taraxa_22.xml b/core/ui/src/main/res/drawable/ic_taraxa_22.xml new file mode 100644 index 0000000000..7ee9b27337 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_taraxa_22.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_flare_22.xml b/core/ui/src/main/res/drawable/img_flare_22.xml new file mode 100644 index 0000000000..50e0da994b --- /dev/null +++ b/core/ui/src/main/res/drawable/img_flare_22.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_mantle_22.xml b/core/ui/src/main/res/drawable/img_mantle_22.xml new file mode 100644 index 0000000000..93dbe6ccb3 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_mantle_22.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_moonriver_22.xml b/core/ui/src/main/res/drawable/img_moonriver_22.xml new file mode 100644 index 0000000000..c2ee947935 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_moonriver_22.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_taraxa_22.xml b/core/ui/src/main/res/drawable/img_taraxa_22.xml new file mode 100644 index 0000000000..3faf5ca9f0 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_taraxa_22.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 2f27ff5330..e3ed6aabed 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -104,6 +104,14 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "nexa" -> Blockchain.Nexa // FIXME "nexa/test" -> Blockchain.NexaTestnet // FIXME "radiant" -> Blockchain.Radiant + "moonriver" -> Blockchain.Moonriver + "moonriver/test" -> Blockchain.MoonriverTestnet + "mantle" -> Blockchain.Mantle + "mantle/test" -> Blockchain.MantleTestnet + "flare-network" -> Blockchain.Flare + "flare-network/test" -> Blockchain.FlareTestnet + "taraxa" -> Blockchain.Taraxa + "taraxa/test" -> Blockchain.TaraxaTestnet else -> null } } @@ -209,6 +217,14 @@ fun Blockchain.toNetworkId(): String { Blockchain.Nexa -> "nexa" // FIXME Blockchain.NexaTestnet -> "nexa/test" // FIXME Blockchain.Radiant -> "radiant" + Blockchain.Moonriver -> "moonriver" + Blockchain.MoonriverTestnet -> "moonriver/test" + Blockchain.Mantle -> "mantle" + Blockchain.MantleTestnet -> "mantle/test" + Blockchain.Flare -> "flare-network" + Blockchain.FlareTestnet -> "flare-network/test" + Blockchain.Taraxa -> "taraxa" + Blockchain.TaraxaTestnet -> "taraxa/test" } } @@ -277,6 +293,10 @@ fun Blockchain.toCoinId(): String { Blockchain.PolygonZkEVM, Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm-ethereum" Blockchain.Nexa, Blockchain.NexaTestnet -> "nexa" // FIXME Blockchain.Radiant -> "radiant" + Blockchain.Moonriver, Blockchain.MoonriverTestnet -> "moonriver" + Blockchain.Mantle, Blockchain.MantleTestnet -> "mantle" + Blockchain.Flare, Blockchain.FlareTestnet -> "flare-networks" + Blockchain.Taraxa, Blockchain.TaraxaTestnet -> "taraxa" } } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 059989055e..5fe435d39f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,7 +85,7 @@ leakcanary = "2.13" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-539" +tangemBlockchainSdk = "develop-546" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-337" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 70eda1b6e704cf659289ab7ccd315178119e9be7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Mar 2024 12:02:46 +0300 Subject: [PATCH 043/109] Updated on 2026-08-14 --- .../com/tangem/tap/common/DialogManager.kt | 4 + .../common/analytics/events/WalletConnect.kt | 6 +- .../tap/domain/walletconnect/BnbHelper.kt | 3 +- .../walletconnect/WalletConnectManager.kt | 9 +- .../walletconnect/WalletConnectSdkHelper.kt | 91 ++++++++++++----- .../walletconnect/extensions/WcPeerMeta.kt | 16 +-- .../app/TangemWcBlockchainHelper.kt | 99 +++++++++++-------- .../app/WalletConnectEventsHandlerImpl.kt | 2 +- ...l.kt => DefaultWalletConnectRepository.kt} | 20 ++-- ...DefaultWalletConnectSessionsRepository.kt} | 6 +- .../di/WalletConnectInteractorModule.kt | 55 ++++++++--- .../domain/WalletConnectInteractor.kt | 12 ++- .../walletconnect2/domain/WcJrpcMethods.kt | 87 +++++++++++----- .../domain/WcPreparedRequest.kt | 17 ++++ .../domain/WcSessionRequestConverter.kt | 18 +++- .../mapper/SolanaTransactionMappings.kt | 30 ++++++ .../domain/models/binance/WcBinanceOrder.kt | 2 +- .../domain/models/solana/SolanaSignMessage.kt | 13 +++ .../models/solana/SolanaTransactionRequest.kt | 42 ++++++++ .../toggles/WalletConnectFeatureToggles.kt | 11 +++ .../walletconnect/WalletConnectMiddleware.kt | 1 + .../redux/walletconnect/WalletConnectState.kt | 8 +- .../dialogs/SignTransactionDialog.kt | 29 ++++++ .../configs/feature_toggles_config.json | 4 + 24 files changed, 445 insertions(+), 140 deletions(-) rename app/src/main/java/com/tangem/tap/domain/walletconnect2/data/{WalletConnectRepositoryImpl.kt => DefaultWalletConnectRepository.kt} (96%) rename app/src/main/java/com/tangem/tap/domain/walletconnect2/data/{WalletConnectSessionsRepositoryImpl.kt => DefaultWalletConnectSessionsRepository.kt} (92%) create mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/mapper/SolanaTransactionMappings.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaSignMessage.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionDialog.kt diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 4f4494f2bc..68fc7580bc 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -121,6 +121,10 @@ class DialogManager : StoreSubscriber { onReject = state.dialog.onReject, ) } + is WalletConnectDialog.SignTransactionDialog -> SignTransactionDialog.create( + preparedData = state.dialog.data, + context = context, + ) is BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context) is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context) is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt index ed47afa57c..6ea8b5b88f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt @@ -5,7 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] */ -sealed class WalletConnect( +internal sealed class WalletConnect( event: String, params: Map = mapOf(), error: Throwable? = null, @@ -65,7 +65,7 @@ sealed class WalletConnect( FAIL("Fail"), } - companion object { - private const val SUCCESS_CODE = "0" + private companion object { + const val SUCCESS_CODE = "0" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt index 9e08d07d76..16a35bf539 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt @@ -14,7 +14,8 @@ import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder import timber.log.Timber -object BnbHelper { +internal object BnbHelper { + fun createMessageData(order: WCBinanceTransferOrder): BinanceMessageData.Transfer { val input = order.msgs.first().inputs.first() val output = order.msgs.first().inputs.first() diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt index b99b599dfd..9078975c2c 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt @@ -41,7 +41,7 @@ import java.util.concurrent.TimeUnit import kotlin.collections.set @Suppress("LargeClass") -class WalletConnectManager { +internal class WalletConnectManager { private var cardId: String? = null @@ -348,6 +348,7 @@ class WalletConnectManager { val hash = WalletConnectSdkHelper().signPersonalMessage( hashToSign = data.hash, networkId = activeData.wallet.blockchain?.toNetworkId() ?: "", + type = data.type, derivationPath = activeData.wallet.derivationPath?.rawPath, cardId = cardId, ) @@ -522,9 +523,9 @@ class WalletConnectManager { } } -typealias Topic = String +internal typealias Topic = String -data class WalletConnectActiveData( +internal data class WalletConnectActiveData( val peerId: String, val remotePeerId: String?, val client: WCClient, @@ -547,7 +548,7 @@ data class WalletConnectActiveData( } @Suppress("MagicNumber") -class RetryInterceptor : Interceptor { +internal class RetryInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request: Request = chain.request() val response = chain.proceed(request) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index a1c5cb5c0b..12dc242d80 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -7,10 +7,7 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumUtils import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.extensions.Result -import com.tangem.blockchain.extensions.SimpleResult -import com.tangem.blockchain.extensions.hexToBigDecimal -import com.tangem.blockchain.extensions.isAscii +import com.tangem.blockchain.extensions.* import com.tangem.common.CompletionResult import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey @@ -25,8 +22,9 @@ import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.toFormattedString import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTradeOrder import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTransferOrder -import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage +import com.tangem.tap.domain.walletconnect2.domain.TransactionType import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction +import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder @@ -45,6 +43,7 @@ import timber.log.Timber import java.math.BigDecimal import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam +@Suppress("LargeClass") class WalletConnectSdkHelper { @Suppress("MagicNumber") @@ -206,8 +205,7 @@ class WalletConnectSdkHelper { walletPublicKey = data.walletManager.wallet.publicKey.seedKey, derivationPath = data.walletManager.wallet.publicKey.derivationPath, ) - val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId) - return when (result) { + return when (val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)) { is CompletionResult.Success -> { val hash = EthereumUtils.prepareTransactionToSend( signature = result.data.signature, @@ -250,8 +248,7 @@ class WalletConnectSdkHelper { walletPublicKey = wallet.publicKey.seedKey, derivationPath = wallet.publicKey.derivationPath, ) - val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId) - return when (result) { + return when (val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)) { is CompletionResult.Success -> { val key = wallet.publicKey.blockchainKey.toDecompressedPublicKey() getBnbResultString( @@ -267,16 +264,17 @@ class WalletConnectSdkHelper { } fun prepareDataForPersonalSign( - message: WcEthereumSignMessage, + message: WcSignMessage, topic: String, metaName: String, id: Long, ): WcPersonalSignData { val messageData = when (message.type) { - WcEthereumSignMessage.WCSignType.MESSAGE, - WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE, + WcSignMessage.WCSignType.MESSAGE, + WcSignMessage.WCSignType.PERSONAL_MESSAGE, -> createMessageData(message) - WcEthereumSignMessage.WCSignType.TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data) + WcSignMessage.WCSignType.TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data) + WcSignMessage.WCSignType.SOLANA_MESSAGE -> message.data.decodeBase58() } val messageString = message.data.hexToAscii() @@ -290,7 +288,7 @@ class WalletConnectSdkHelper { id = id, ) return WcPersonalSignData( - hash = messageData, + hash = requireNotNull(messageData) { "Message data must not be null" }, topic = topic, id = id, dialogData = dialogData, @@ -298,7 +296,7 @@ class WalletConnectSdkHelper { ) } - private fun createMessageData(message: WcEthereumSignMessage): ByteArray { + private fun createMessageData(message: WcSignMessage): ByteArray { val messageData = try { message.data.removePrefix(HEX_PREFIX).hexToBytes() } catch (exception: Exception) { @@ -332,6 +330,7 @@ class WalletConnectSdkHelper { suspend fun signPersonalMessage( hashToSign: ByteArray, networkId: String, + type: WcSignMessage.WCSignType, derivationPath: String?, cardId: String?, ): String? { @@ -345,12 +344,16 @@ class WalletConnectSdkHelper { ) return when (val result = tangemSdkManager.runTaskAsync(command, cardId)) { is CompletionResult.Success -> { - val hash = result.data.signature - return EthereumUtils.prepareSignedMessageData( - signedHash = hash, - hashToSign = hashToSign, - publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(), - ) + val signedHash = result.data.signature + + return when (type) { + WcSignMessage.WCSignType.SOLANA_MESSAGE -> getSolanaResultString(signedHash) + else -> EthereumUtils.prepareSignedMessageData( + signedHash = signedHash, + hashToSign = hashToSign, + publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(), + ) + } } is CompletionResult.Failure -> { Timber.e(result.error.customMessage) @@ -359,12 +362,46 @@ class WalletConnectSdkHelper { } } - companion object { - private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n" - private const val HEX_PREFIX = "0x" - private const val DEFAULT_MAX_GASLIMIT = 350000 - fun getBnbResultString(publicKey: String, signature: String): String { - return "{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}" + suspend fun signTransaction( + hashToSign: ByteArray, + networkId: String, + type: TransactionType, + derivationPath: String?, + cardId: String?, + ): String? { + val blockchain = Blockchain.fromNetworkId(networkId) ?: return null + val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null + + val command = SignHashCommand( + hash = hashToSign, + walletPublicKey = wallet.publicKey.seedKey, + derivationPath = wallet.publicKey.derivationPath, + ) + return when (val result = tangemSdkManager.runTaskAsync(command, cardId)) { + is CompletionResult.Success -> { + val signedHash = result.data.signature + + when (type) { + TransactionType.SOLANA_TX -> { + getSolanaResultString(signedHash) + } + } + } + is CompletionResult.Failure -> { + Timber.e(result.error.customMessage) + null + } } } + + private fun getSolanaResultString(signedHash: ByteArray) = "{ signature: \"${signedHash.encodeBase58()}\" }" + + private fun getBnbResultString(publicKey: String, signature: String) = + "{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}" + + private companion object { + const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n" + const val HEX_PREFIX = "0x" + const val DEFAULT_MAX_GASLIMIT = 350000 + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt index 3197f2729c..d227a42923 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt @@ -1,18 +1,18 @@ package com.tangem.tap.domain.walletconnect.extensions -import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction +import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage import com.trustwallet.walletconnect.models.WCPeerMeta import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction -fun WCPeerMeta.isDappSupported(): Boolean { +internal fun WCPeerMeta.isDappSupported(): Boolean { return !unsupportedDappsList.any { this.url.contains(it) } } private val unsupportedDappsList: List = listOf("dydx.exchange") -fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction { +internal fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction { return WcEthereumTransaction( from = from, to = to, @@ -27,13 +27,13 @@ fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction { ) } -fun WCEthereumSignMessage.toWcEthereumSignMessage(): WcEthereumSignMessage { - return WcEthereumSignMessage( +internal fun WCEthereumSignMessage.toWcEthereumSignMessage(): WcSignMessage { + return WcSignMessage( raw = raw, type = when (type) { - WCEthereumSignMessage.WCSignType.MESSAGE -> WcEthereumSignMessage.WCSignType.MESSAGE - WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -> WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE - WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -> WcEthereumSignMessage.WCSignType.TYPED_MESSAGE + WCEthereumSignMessage.WCSignType.MESSAGE -> WcSignMessage.WCSignType.MESSAGE + WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -> WcSignMessage.WCSignType.PERSONAL_MESSAGE + WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -> WcSignMessage.WCSignType.TYPED_MESSAGE }, ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt index 3b69331ae2..63e68ba973 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt @@ -4,8 +4,18 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toNetworkId import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper +import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles + +internal class TangemWcBlockchainHelper( + private val featureToggles: WalletConnectFeatureToggles, +) : WcBlockchainHelper { + + private val supportedNonEvmBlockchains = if (featureToggles.isSolanaTxSignEnabled) { + setOf(Blockchain.Solana, Blockchain.SolanaTestnet) + } else { + emptySet() + } -class TangemWcBlockchainHelper : WcBlockchainHelper { override fun chainIdToNetworkIdOrNull(chainId: String): String? { val parsedId = chainId.parseId() ?: return null val blockchain = parsedId.chainIdToBlockchain() @@ -41,48 +51,57 @@ class TangemWcBlockchainHelper : WcBlockchainHelper { val networkId = chainIdToNetworkIdOrNull(chainId) ?: return null return Blockchain.fromNetworkId(networkId)?.fullName } -} -private fun Blockchain.getCaip2ChainId(): String? { - if (this.isEvm()) return this.getChainId()?.toString() + private fun Blockchain.getCaip2ChainId(): String? { + if (this.isEvm()) return this.getChainId()?.toString() - return when (this) { - Blockchain.Solana -> "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ" - Blockchain.Polkadot -> "91b171bb158e2d3848fa23a9f1c25182" - Blockchain.Tron -> "0x2b6653dc" - else -> null - } -} - -private fun Blockchain.getCaip2Namespace(): String? { - return when { - this.isEvm() -> EVM_NAMESPACE - supportedNonEvmBlockchains.contains(this) -> this.toNetworkId() - else -> null - } -} - -private fun String.parseId(): Pair? { - val parsed = this.split(CHAIN_SEPARATOR) - if (parsed.size != 2) return null - - return parsed[0] to parsed[1] -} - -private fun Pair.chainIdToBlockchain(): Blockchain? { - return when (first) { - EVM_NAMESPACE -> { - second.toIntOrNull() - ?.let(Blockchain::fromChainId) - } - else -> { - Blockchain.fromNetworkId(networkId = first) - .takeIf(supportedNonEvmBlockchains::contains) + return when (this) { + /* + * The WC sample application and documentation use a commented out chain ID. However, in real dApps + * uncommented is used. + * Docs: https://docs.walletconnect.com/advanced/multichain/chain-list + * + * Blockchain.Solana -> "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + * */ + Blockchain.Solana -> "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ" + Blockchain.SolanaTestnet -> "z4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z" + Blockchain.Polkadot -> "91b171bb158e2d3848fa23a9f1c25182" + Blockchain.Tron -> "0x2b6653dc" + else -> null } } -} -private val supportedNonEvmBlockchains = emptySet() // TODO: add supported networks + private fun Blockchain.getCaip2Namespace(): String? { + return when { + this.isEvm() -> EVM_NAMESPACE + supportedNonEvmBlockchains.contains(this) -> this.toNetworkId().substringBefore(TESTNET_SEPARATOR) + else -> null + } + } -private const val EVM_NAMESPACE = "eip155" -private const val CHAIN_SEPARATOR = ":" \ No newline at end of file + private fun String.parseId(): Pair? { + val parsed = this.split(CHAIN_SEPARATOR) + if (parsed.size != 2) return null + + return parsed[0] to parsed[1] + } + + private fun Pair.chainIdToBlockchain(): Blockchain? { + return when (first) { + EVM_NAMESPACE -> { + second.toIntOrNull() + ?.let(Blockchain::fromChainId) + } + else -> { + Blockchain.fromNetworkId(networkId = first) + .takeIf(supportedNonEvmBlockchains::contains) + } + } + } + + private companion object { + const val EVM_NAMESPACE = "eip155" + const val CHAIN_SEPARATOR = ":" + const val TESTNET_SEPARATOR = "/" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt index d6767ddc8e..909f4dafb7 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt @@ -12,7 +12,7 @@ import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen import com.tangem.tap.store import timber.log.Timber -class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler { +internal class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler { override fun onProposalReceived(proposal: WalletConnectEvents.SessionProposal, networksFormatted: String) { store.dispatchOnMain( GlobalAction.ShowDialog( diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt similarity index 96% rename from app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt rename to app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt index 71f910ce82..46577399e9 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt @@ -18,9 +18,8 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import timber.log.Timber -import javax.inject.Inject -class WalletConnectRepositoryImpl @Inject constructor( +internal class DefaultWalletConnectRepository( private val application: Application, private val wcRequestDeserializer: WcJrpcRequestsDeserializer, private val analyticsHandler: AnalyticsEventHandler, @@ -95,11 +94,11 @@ class WalletConnectRepositoryImpl @Inject constructor( ) { // Triggered when wallet receives the session proposal sent by a Dapp Timber.d("sessionProposal: $sessionProposal") - this@WalletConnectRepositoryImpl.sessionProposal = sessionProposal + this@DefaultWalletConnectRepository.sessionProposal = sessionProposal val missingNetworks = findMissingNetworks( namespaces = sessionProposal.requiredNamespaces, - userNamespaces = this@WalletConnectRepositoryImpl.userNamespaces ?: emptyMap(), + userNamespaces = this@DefaultWalletConnectRepository.userNamespaces ?: emptyMap(), ) if (missingNetworks.isNotEmpty()) { @@ -116,7 +115,7 @@ class WalletConnectRepositoryImpl @Inject constructor( val optionalWithoutMissingNetworks = removeMissingNetworks( namespaces = sessionProposal.optionalNamespaces, - userNamespaces = this@WalletConnectRepositoryImpl.userNamespaces ?: emptyMap(), + userNamespaces = this@DefaultWalletConnectRepository.userNamespaces ?: emptyMap(), ) scope.launch { @@ -334,6 +333,7 @@ class WalletConnectRepositoryImpl @Inject constructor( ), ) } + Web3Wallet.respondSessionRequest( params = Wallet.Params.SessionRequestResponse( sessionTopic = requestData.topic, @@ -342,15 +342,19 @@ class WalletConnectRepositoryImpl @Inject constructor( result = result, ), ), - onSuccess = {}, - onError = { + onSuccess = { response -> + Timber.d("Session request responded successfully: $response") + }, + onError = { error -> + Timber.d(error.throwable, "Error while responging session request") + WalletConnect.RequestHandledParams( dAppName = session?.name ?: "", dAppUrl = session?.url ?: "", methodName = requestData.method, blockchain = requestData.blockchain, errorCode = WalletConnectError.ValidationError.error, - errorDescription = it.throwable.message, + errorDescription = error.throwable.message, ) }, ) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectSessionsRepositoryImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt similarity index 92% rename from app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectSessionsRepositoryImpl.kt rename to app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt index 99f4351a93..77b531934a 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectSessionsRepositoryImpl.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt @@ -3,15 +3,13 @@ package com.tangem.tap.domain.walletconnect2.data import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types -import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.files.FileReader import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.domain.walletconnect2.domain.models.Session import timber.log.Timber -import javax.inject.Inject -class WalletConnectSessionsRepositoryImpl @Inject constructor( - @SdkMoshi private val moshi: Moshi, +internal class DefaultWalletConnectSessionsRepository( + private val moshi: Moshi, private val fileReader: FileReader, ) : WalletConnectSessionsRepository { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index 8cf7905260..927eab61da 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -1,15 +1,22 @@ package com.tangem.tap.domain.walletconnect2.di +import android.app.Application +import com.squareup.moshi.Moshi +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.datasource.di.SdkMoshi +import com.tangem.datasource.files.FileReader import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl -import com.tangem.tap.domain.walletconnect2.data.WalletConnectRepositoryImpl -import com.tangem.tap.domain.walletconnect2.data.WalletConnectSessionsRepositoryImpl +import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectRepository +import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository +import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer +import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider -import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,19 +27,21 @@ import javax.inject.Singleton @Module @InstallIn(ActivityComponent::class) -class WalletConnectInteractorModule { +internal object WalletConnectInteractorModule { + @Provides @ActivityScoped fun provideWalletConnectInteractor( wcRepository: WalletConnectRepository, wcSessionsRepository: WalletConnectSessionsRepository, + walletConnectFeatureToggles: WalletConnectFeatureToggles, ): WalletConnectInteractor { return WalletConnectInteractor( handler = WalletConnectEventsHandlerImpl(), walletConnectRepository = wcRepository, sessionsRepository = wcSessionsRepository, sdkHelper = WalletConnectSdkHelper(), - blockchainHelper = TangemWcBlockchainHelper(), + blockchainHelper = TangemWcBlockchainHelper(walletConnectFeatureToggles), dispatcher = AppCoroutineDispatcherProvider(), ) } @@ -40,15 +49,37 @@ class WalletConnectInteractorModule { @Module @InstallIn(SingletonComponent::class) -interface WalletConnectModule { +internal object WalletConnectModule { - @Binds + @Provides @Singleton - fun bindWalletConnectRepository(repository: WalletConnectRepositoryImpl): WalletConnectRepository + fun provideWalletConnectFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletConnectFeatureToggles { + return WalletConnectFeatureToggles(featureTogglesManager) + } - @Binds + @Provides @Singleton - fun bindWalletConnectSessionsRepository( - repository: WalletConnectSessionsRepositoryImpl, - ): WalletConnectSessionsRepository + fun provideWalletConnectRepository( + application: Application, + wcRequestDeserializer: WcJrpcRequestsDeserializer, + analyticsHandler: AnalyticsEventHandler, + ): WalletConnectRepository { + return DefaultWalletConnectRepository( + application = application, + wcRequestDeserializer = wcRequestDeserializer, + analyticsHandler = analyticsHandler, + ) + } + + @Provides + @Singleton + fun provideWalletConnectSessionsRepository( + @SdkMoshi moshi: Moshi, + fileReader: FileReader, + ): WalletConnectSessionsRepository { + return DefaultWalletConnectSessionsRepository( + moshi = moshi, + fileReader = fileReader, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index 884d3ac5d7..3bf99ffd83 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -209,7 +209,7 @@ class WalletConnectInteractor( val currentRequest = this.currentRequest if (currentRequest == null || request.topic != currentRequest.topic) return - val networkId = blockchainHelper.chainIdToNetworkIdOrNull(currentRequest.chainId ?: "") ?: return + val networkId = blockchainHelper.chainIdToNetworkIdOrNull(currentRequest.chainId.orEmpty()) ?: return val signedHash = when (request) { is WcPreparedRequest.BnbTransaction -> sdkHelper.signBnbTransaction( data = request.preparedRequestData.data.data, @@ -224,13 +224,19 @@ class WalletConnectInteractor( is WcPreparedRequest.EthSign -> sdkHelper.signPersonalMessage( hashToSign = request.preparedRequestData.hash, networkId = networkId, + type = request.preparedRequestData.type, + derivationPath = request.derivationPath, + cardId = cardId, + ) + is WcPreparedRequest.SignTransaction -> sdkHelper.signTransaction( + hashToSign = request.preparedRequestData.hashToSign, + networkId = networkId, + type = request.preparedRequestData.type, derivationPath = request.derivationPath, cardId = cardId, ) } - Timber.d("Signed hash: $signedHash") - val requestData = RequestData( topic = request.topic, requestId = request.requestId, diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt index 9a0da6c971..3cb1187509 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt @@ -2,15 +2,17 @@ package com.tangem.tap.domain.walletconnect2.domain import com.squareup.moshi.* import com.tangem.datasource.di.SdkMoshi -import com.tangem.tap.domain.walletconnect2.domain.models.binance.WCBinanceTxConfirmParam import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceCancelOrder import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder +import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTxConfirmParam +import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaSignMessage +import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaTransactionRequest import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton -enum class WcJrpcMethods(val code: String) { +internal enum class WcJrpcMethods(val code: String) { ETH_SIGN("eth_sign"), ETH_PERSONAL_SIGN("personal_sign"), @@ -18,10 +20,14 @@ enum class WcJrpcMethods(val code: String) { ETH_SIGN_TYPE_DATA_V4("eth_signTypedData_v4"), ETH_SIGN_TRANSACTION("eth_signTransaction"), ETH_SEND_TRANSACTION("eth_sendTransaction"), + BNB_SIGN("bnb_sign"), BNB_TRANSACTION_CONFIRM("bnb_tx_confirmation"), SIGN_TRANSACTION("trust_signTransaction"), WALLET_ADD_ETHEREUM_CHAIN("wallet_addEthereumChain"), + + SOLANA_SIGN_TX("solana_signTransaction"), + SOLANA_SIGN_MESSAGE("solana_signMessage"), ; companion object { @@ -30,7 +36,7 @@ enum class WcJrpcMethods(val code: String) { } @JsonClass(generateAdapter = true) -data class WCSignTransaction( +data class WcSignTransaction( @Json(name = "network") val network: Int, @@ -39,13 +45,7 @@ data class WCSignTransaction( ) : WcRequestData @JsonClass(generateAdapter = true) -data class WcAddChain( - @Json(name = "chainId") - val chainId: String, -) : WcRequestData - -@JsonClass(generateAdapter = true) -data class WcEthereumSignMessage( +data class WcSignMessage( @Json(name = "raw") val raw: List, @@ -53,7 +53,7 @@ data class WcEthereumSignMessage( val type: WCSignType, ) : WcRequestData { enum class WCSignType { - MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE + MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE, SOLANA_MESSAGE, } /** @@ -63,6 +63,7 @@ data class WcEthereumSignMessage( * - MESSAGE: `[address, data ]` * - TYPED_MESSAGE: `[address, data]` * - PERSONAL_MESSAGE: `[data, address]` + * - SOLANA_MESSAGE: `[publicKey (address), message]` * * reference: https://docs.walletconnect.org/json-rpc/ethereum#eth_signtypeddata */ @@ -79,6 +80,21 @@ data class WcEthereumSignMessage( } } +@JsonClass(generateAdapter = true) +data class WcSignMessageData( + @Json(name = "address") + val address: String, + + @Json(name = "message") + val message: String, +) : WcRequestData + +@JsonClass(generateAdapter = true) +data class WcAddChain( + @Json(name = "chainId") + val chainId: String, +) : WcRequestData + @JsonClass(generateAdapter = true) data class WcEthereumTransaction( @Json(name = "from") @@ -117,20 +133,21 @@ interface WcRequestData data class WcCustomRequestData(val data: String) : WcRequestData sealed class WcRequest(open val data: WcRequestData) { - data class EthSign(override val data: WcEthereumSignMessage) : WcRequest(data) + data class EthSign(override val data: WcSignMessage) : WcRequest(data) data class EthSignTransaction(override val data: WcEthereumTransaction) : WcRequest(data) data class EthSendTransaction(override val data: WcEthereumTransaction) : WcRequest(data) data class BnbTrade(override val data: WcBinanceTradeOrder) : WcRequest(data) data class BnbCancel(override val data: WcBinanceCancelOrder) : WcRequest(data) data class BnbTransfer(override val data: WcBinanceTransferOrder) : WcRequest(data) - data class BnbTxConfirm(override val data: WCBinanceTxConfirmParam) : WcRequest(data) - data class SignTransaction(override val data: WCSignTransaction) : WcRequest(data) + data class BnbTxConfirm(override val data: WcBinanceTxConfirmParam) : WcRequest(data) + data class SignTransaction(override val data: WcSignTransaction) : WcRequest(data) data class AddChain(override val data: WcAddChain) : WcRequest(data) data class CustomRequest(override val data: WcCustomRequestData) : WcRequest(data) + data class SolanaSignRequest(override val data: SolanaTransactionRequest) : WcRequest(data) } @Singleton -class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi: Moshi) { +internal class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi: Moshi) { @Suppress("ComplexMethod", "LongMethod") fun deserialize(method: String, params: String): WcRequest { @@ -154,9 +171,9 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi val deserializedParams = moshi.adapter>( Types.newParameterizedType(List::class.java, String::class.java), ).fromJsonOrNull(params) ?: return customRequest - val data = WcEthereumSignMessage( + val data = WcSignMessage( raw = deserializedParams, - type = WcEthereumSignMessage.WCSignType.MESSAGE, + type = WcSignMessage.WCSignType.MESSAGE, ) WcRequest.EthSign(data = data) } @@ -164,9 +181,9 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi val deserializedParams = moshi.adapter>( Types.newParameterizedType(List::class.java, String::class.java), ).fromJsonOrNull(params) ?: return customRequest - val data = WcEthereumSignMessage( + val data = WcSignMessage( raw = deserializedParams, - type = WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE, + type = WcSignMessage.WCSignType.PERSONAL_MESSAGE, ) WcRequest.EthSign(data = data) } @@ -175,9 +192,9 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi params.substring(params.indexOf("\"") + 1, params.indexOf("\"", startIndex = 2)), params.substring(params.indexOfFirst { it == '{' }, params.indexOfLast { it == '}' } + 1), ) - val data = WcEthereumSignMessage( + val data = WcSignMessage( deserializedParams, - WcEthereumSignMessage.WCSignType.TYPED_MESSAGE, + WcSignMessage.WCSignType.TYPED_MESSAGE, ) Timber.d("TypedData params: $deserializedParams") WcRequest.EthSign(data) @@ -186,23 +203,43 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi return deserializeBnb(moshi, params) ?: customRequest } WcJrpcMethods.BNB_TRANSACTION_CONFIRM -> { - val deserializedParams = moshi.adapter>( - Types.newParameterizedType(List::class.java, WCBinanceTxConfirmParam::class.java), + val deserializedParams = moshi.adapter>( + Types.newParameterizedType(List::class.java, WcBinanceTxConfirmParam::class.java), ).fromJsonFirstOrNull(params) ?: return customRequest WcRequest.BnbTxConfirm(data = deserializedParams) } WcJrpcMethods.SIGN_TRANSACTION -> { - val deserializedParams = moshi.adapter>( - Types.newParameterizedType(List::class.java, WCSignTransaction::class.java), + val deserializedParams = moshi.adapter>( + Types.newParameterizedType(List::class.java, WcSignTransaction::class.java), ).fromJsonFirstOrNull(params) ?: return customRequest + WcRequest.SignTransaction(data = deserializedParams) } WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN -> { val deserializedParams: WcAddChain = moshi.adapter>( Types.newParameterizedType(List::class.java, WcAddChain::class.java), ).fromJsonFirstOrNull(params) ?: return customRequest + WcRequest.AddChain(data = deserializedParams) } + WcJrpcMethods.SOLANA_SIGN_TX -> { + val tx = moshi.adapter(SolanaTransactionRequest::class.java) + .fromJsonOrNull(params) + ?: return customRequest + + WcRequest.SolanaSignRequest(data = tx) + } + WcJrpcMethods.SOLANA_SIGN_MESSAGE -> { + val signMessage = moshi.adapter(SolanaSignMessage::class.java) + .fromJsonOrNull(params) + ?: return customRequest + val data = WcSignMessage( + raw = listOf(signMessage.publicKey, signMessage.message), + type = WcSignMessage.WCSignType.SOLANA_MESSAGE, + ) + + WcRequest.EthSign(data = data) + } } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt index be17cc867b..64798e2d9c 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt @@ -30,4 +30,21 @@ sealed class WcPreparedRequest( requestId: Long, derivationPath: String?, ) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath) + + class SignTransaction( + override val preparedRequestData: WcGenericTransactionData, + topic: String, + requestId: Long, + derivationPath: String?, + ) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath) +} + +data class WcGenericTransactionData( + val hashToSign: ByteArray, + val dAppName: String, + val type: TransactionType, +) + +enum class TransactionType { + SOLANA_TX, } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt index fe67571134..12085cad12 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt @@ -1,12 +1,13 @@ package com.tangem.tap.domain.walletconnect2.domain import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper +import com.tangem.tap.domain.walletconnect2.domain.mapper.mapToTransaction import com.tangem.tap.domain.walletconnect2.domain.models.BnbData import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType -class WcSessionRequestConverter( +internal class WcSessionRequestConverter( private val blockchainHelper: WcBlockchainHelper, private val sessionsRepository: WalletConnectSessionsRepository, private val sdkHelper: WalletConnectSdkHelper, @@ -107,6 +108,20 @@ class WcSessionRequestConverter( derivationPath = derivationPath, ) } + is WcRequest.SolanaSignRequest -> { + val data = request.data.mapToTransaction() + + WcPreparedRequest.SignTransaction( + preparedRequestData = WcGenericTransactionData( + hashToSign = data.getSerializedMessage(), + dAppName = sessionRequest.metaName, + type = TransactionType.SOLANA_TX, + ), + topic = sessionRequest.topic, + requestId = sessionRequest.id, + derivationPath = derivationPath, + ) + } else -> null } } @@ -118,6 +133,7 @@ class WcSessionRequestConverter( is WcRequest.EthSendTransaction -> request.data.from is WcRequest.EthSignTransaction -> request.data.from is WcRequest.EthSign -> request.data.address + is WcRequest.SolanaSignRequest -> request.data.feePayer else -> null } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/mapper/SolanaTransactionMappings.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/mapper/SolanaTransactionMappings.kt new file mode 100644 index 0000000000..c97506bb7e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/mapper/SolanaTransactionMappings.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.domain.walletconnect2.domain.mapper + +import com.tangem.blockchain.blockchains.solana.solanaj.core.SolanaTransaction +import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaTransactionRequest +import org.p2p.solanaj.core.AccountMeta +import org.p2p.solanaj.core.PublicKey +import org.p2p.solanaj.core.TransactionInstruction + +internal fun SolanaTransactionRequest.mapToTransaction(): SolanaTransaction { + val from = PublicKey(feePayer) + val instructions = instructions.map(SolanaTransactionRequest.Instruction::mapToInstruction) + + return SolanaTransaction(from) + .apply { + instructions.forEach(::addInstruction) + setRecentBlockHash(recentBlockhash) + } +} + +private fun SolanaTransactionRequest.Instruction.mapToInstruction() = TransactionInstruction( + /* programId = */ PublicKey(programId), + /* keys = */ keys.map(SolanaTransactionRequest.Key::mapToAccountMeta), + /* data = */ data.toByteArray(), +) + +private fun SolanaTransactionRequest.Key.mapToAccountMeta() = AccountMeta( + /* publicKey = */ PublicKey(publicKey), + /* isSigner = */ isSigner, + /* isWritable = */ isWritable, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt index 8325aadd72..1e3f33db29 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt @@ -13,7 +13,7 @@ open class WcBinanceOrder( val msgs: List, ) : WcRequestData -data class WCBinanceTxConfirmParam( +data class WcBinanceTxConfirmParam( val ok: Boolean, val errorMsg: String?, ) : WcRequestData \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaSignMessage.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaSignMessage.kt new file mode 100644 index 0000000000..a521aa55e9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaSignMessage.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.domain.walletconnect2.domain.models.solana + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SolanaSignMessage( + @Json(name = "pubkey") + val publicKey: String, + + @Json(name = "message") + val message: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt new file mode 100644 index 0000000000..7ea66042f4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt @@ -0,0 +1,42 @@ +package com.tangem.tap.domain.walletconnect2.domain.models.solana + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.tap.domain.walletconnect2.domain.WcRequestData + +@JsonClass(generateAdapter = true) +data class SolanaTransactionRequest( + @Json(name = "feePayer") + val feePayer: String, + + @Json(name = "recentBlockhash") + val recentBlockhash: String, + + @Json(name = "instructions") + val instructions: List, +) : WcRequestData { + + @JsonClass(generateAdapter = true) + data class Instruction( + @Json(name = "programId") + val programId: String, + + @Json(name = "data") + val data: List, + + @Json(name = "keys") + val keys: List, + ) + + @JsonClass(generateAdapter = true) + data class Key( + @Json(name = "isSigner") + val isSigner: Boolean, + + @Json(name = "isWritable") + val isWritable: Boolean, + + @Json(name = "pubkey") + val publicKey: String, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt new file mode 100644 index 0000000000..26547d68e8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.domain.walletconnect2.toggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class WalletConnectFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) { + + val isSolanaTxSignEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "WC_SOLANA_TX_SIGN_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index c45ec9dae3..47c26d8452 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -375,6 +375,7 @@ class WalletConnectMiddleware { is WcPreparedRequest.BnbTransaction -> WalletConnectDialog.BnbTransactionDialog(request) is WcPreparedRequest.EthTransaction -> WalletConnectDialog.RequestTransaction(request) is WcPreparedRequest.EthSign -> WalletConnectDialog.PersonalSign(request) + is WcPreparedRequest.SignTransaction -> WalletConnectDialog.SignTransactionDialog(request) } store.dispatch(GlobalAction.ShowDialog(dialog)) } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index 34eed45fd3..c696a29485 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -8,8 +8,8 @@ import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.core.navigation.StateDialog import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest +import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData @@ -117,6 +117,10 @@ sealed class WalletConnectDialog : StateDialog { data class BnbTransactionDialog( val data: WcPreparedRequest.BnbTransaction, ) : WalletConnectDialog() + + data class SignTransactionDialog( + val data: WcPreparedRequest.SignTransaction, + ) : WalletConnectDialog() } data class WcTransactionData( @@ -138,7 +142,7 @@ data class WcPersonalSignData( val topic: String, val id: Long, val dialogData: PersonalSignDialogData, - val type: WcEthereumSignMessage.WCSignType, + val type: WcSignMessage.WCSignType, ) sealed class BinanceMessageData( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionDialog.kt new file mode 100644 index 0000000000..a21b7a4d75 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionDialog.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.features.details.ui.walletconnect.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.store +import com.tangem.wallet.R + +internal object SignTransactionDialog { + + fun create(preparedData: WcPreparedRequest.SignTransaction, context: Context): AlertDialog { + val message = context.getString(R.string.wallet_connect_alert_sign_message, "") + return AlertDialog.Builder(context).apply { + setTitle(context.getString(R.string.wallet_connect_title)) + setMessage(message) + setPositiveButton(context.getText(R.string.common_sign)) { _, _ -> + store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) + } + setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> + store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId)) + } + setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + }.create() + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 8f979c41f0..c3c879c727 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -22,5 +22,9 @@ { "name": "GENERATE_XPUB_ENABLED", "version": "undefined" + }, + { + "name": "WC_SOLANA_TX_SIGN_ENABLED", + "version": "5.9.0" } ] From 5516a0aa333b83601f220ab390fba74728cab0cd Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Mar 2024 17:42:38 +0500 Subject: [PATCH 044/109] Updated on 2026-08-14 --- .../ui/components/rows/SelectorRowItem.kt | 110 ++++++++-------- .../presentation/ui/fee/SendSpeedSelector.kt | 118 ++++-------------- .../impl/presentation/ui/send/FeeBlock.kt | 56 +++------ .../feature/swap/ui/ChooseFeeBottomSheet.kt | 32 +++-- .../tangem/feature/swap/ui/ProviderItem.kt | 41 +++--- .../feature/swap/ui/SwapScreenContent.kt | 9 +- 6 files changed, 136 insertions(+), 230 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt index 667ec44c08..bedec66060 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt @@ -14,11 +14,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -27,12 +27,14 @@ import com.tangem.core.ui.res.TangemTheme fun SelectorRowItem( @StringRes titleRes: Int, @DrawableRes iconRes: Int, - onSelect: () -> Unit, modifier: Modifier = Modifier, - preEllipsize: TextReference? = null, - postEllipsize: TextReference? = null, + paddingValues: PaddingValues = PaddingValues(TangemTheme.dimens.spacing12), + preDot: TextReference? = null, + postDot: TextReference? = null, + ellipsizeOffset: Int? = null, isSelected: Boolean = false, showDivider: Boolean = true, + onSelect: (() -> Unit)? = null, ) { val iconTint by animateColorAsState( targetValue = if (isSelected) { @@ -43,45 +45,39 @@ fun SelectorRowItem( label = "Selector icon tint change", ) - val textStyle = if (isSelected) { - TangemTheme.typography.subtitle2 - } else { - TangemTheme.typography.body2 - } - Box( modifier = modifier .fillMaxWidth() - .clickable { onSelect() }, + .then( + if (onSelect != null) { + Modifier.clickable { onSelect() } + } else { + Modifier + }, + ), ) { - Row(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues), + verticalAlignment = Alignment.CenterVertically, + ) { Icon( painter = painterResource(iconRes), tint = iconTint, contentDescription = null, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ), ) Text( text = stringResource(titleRes), - style = textStyle, + style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing8, - top = TangemTheme.dimens.spacing14, - bottom = TangemTheme.dimens.spacing14, - ), + modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), ) - if (preEllipsize != null && postEllipsize != null) { + if (preDot != null && postDot != null) { SelectorValueContent( - amount = preEllipsize, - symbol = postEllipsize, - textStyle = textStyle, + preDot = preDot, + postDot = postDot, + ellipsizeOffset = ellipsizeOffset, ) } } @@ -99,33 +95,37 @@ fun SelectorRowItem( } @Composable -private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) { - Text( - text = amount.resolveReference(), - style = textStyle, +private fun RowScope.SelectorValueContent( + preDot: TextReference, + postDot: TextReference, + ellipsizeOffset: Int? = null, +) { + val ellipsis = if (ellipsizeOffset == null) { + TextEllipsis.End + } else { + TextEllipsis.OffsetEnd(ellipsizeOffset) + } + EllipsisText( + text = preDot.resolveReference(), + style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.End, - overflow = TextOverflow.Ellipsis, - maxLines = 1, + ellipsis = ellipsis, modifier = Modifier .weight(1f) - .padding( - start = TangemTheme.dimens.spacing4, - top = TangemTheme.dimens.spacing14, - bottom = TangemTheme.dimens.spacing14, - ), + .padding(start = TangemTheme.dimens.spacing4), ) Text( - text = symbol.resolveReference(), - style = textStyle, + text = "•", + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing1, - end = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing14, - bottom = TangemTheme.dimens.spacing14, - ), + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing4), + ) + Text( + text = postDot.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, ) } @@ -136,8 +136,9 @@ private fun SelectorRowItemPreview_Light() { SelectorRowItem( titleRes = R.string.common_fee_selector_option_slow, iconRes = R.drawable.ic_tortoise_24, - preEllipsize = TextReference.Str("1000"), - postEllipsize = TextReference.Str("$"), + preDot = TextReference.Str("1000 ETH"), + postDot = TextReference.Str("1000 $"), + ellipsizeOffset = 4, isSelected = true, onSelect = { }, ) @@ -151,8 +152,9 @@ private fun SelectorRowItemPreview_Dark() { SelectorRowItem( titleRes = R.string.common_fee_selector_option_slow, iconRes = R.drawable.ic_tortoise_24, - preEllipsize = TextReference.Str("1000"), - postEllipsize = TextReference.Str("$"), + preDot = TextReference.Str("1000 ETH"), + postDot = TextReference.Str("1000 $"), + ellipsizeOffset = 4, isSelected = true, onSelect = { }, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index f5b10f50a8..e0a3868ec1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -3,7 +3,6 @@ package com.tangem.features.send.impl.presentation.ui.fee import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateColorAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image @@ -16,7 +15,6 @@ 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.draw.clip import androidx.compose.ui.graphics.Color @@ -33,11 +31,9 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.components.atoms.text.EllipsisText -import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter @@ -267,55 +263,23 @@ private fun SendSpeedSelectorItem( showDivider: Boolean = true, showWarning: Boolean = false, ) { - val iconTint by animateColorAsState( - targetValue = if (isSelected) { - TangemTheme.colors.icon.accent - } else { - TangemTheme.colors.icon.informative - }, - label = "Selector icon tint change", - ) - - val textStyle = if (isSelected) { - TangemTheme.typography.subtitle2 - } else { - TangemTheme.typography.body2 - } - Box( modifier = modifier .fillMaxWidth() .clickable { onSelect() }, ) { - Row(modifier = Modifier.fillMaxWidth()) { - SelectorTitleContent( - titleRes = titleRes, - iconRes = iconRes, - iconTint = iconTint, - textStyle = textStyle, - ) - if (amount != null && symbolLength != null && fiatAmount != null) { - SelectorValueContent( - amount = amount, - fiatAmount = fiatAmount, - symbolLength = symbolLength, - textStyle = textStyle, - ) - } else { - SpacerWMax() - } - WarningIcon(showWarning = showWarning) - } - if (showDivider) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size1) - .padding(horizontal = TangemTheme.dimens.spacing12) - .background(TangemTheme.colors.stroke.primary) - .align(Alignment.BottomCenter), - ) - } + SelectorRowItem( + titleRes = titleRes, + iconRes = iconRes, + onSelect = onSelect, + modifier = modifier, + preDot = amount, + postDot = fiatAmount, + ellipsizeOffset = symbolLength, + isSelected = isSelected, + showDivider = showDivider, + ) + WarningIcon(showWarning = showWarning) } } @@ -350,41 +314,6 @@ private fun SelectorTitleContent( ) } -@Composable -private fun RowScope.SelectorValueContent( - amount: TextReference, - fiatAmount: TextReference, - symbolLength: Int, - textStyle: TextStyle, -) { - EllipsisText( - text = amount.resolveReference(), - style = textStyle, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.End, - ellipsis = TextEllipsis.OffsetEnd(symbolLength), - modifier = Modifier - .weight(1f) - .padding( - start = TangemTheme.dimens.spacing4, - top = TangemTheme.dimens.spacing14, - bottom = TangemTheme.dimens.spacing14, - ), - ) - Text( - text = "(${fiatAmount.resolveReference()})", - style = textStyle, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing4, - end = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing14, - bottom = TangemTheme.dimens.spacing14, - ), - ) -} - @Composable private fun WarningIcon(showWarning: Boolean = false) { AnimatedVisibility( @@ -393,15 +322,18 @@ private fun WarningIcon(showWarning: Boolean = false) { enter = fadeIn(), exit = fadeOut(), ) { - Image( - painter = painterResource(R.drawable.ic_alert_triangle_20), - contentDescription = null, - modifier = Modifier - .padding( - vertical = TangemTheme.dimens.spacing12, - horizontal = TangemTheme.dimens.spacing14, - ), - ) + Row { + SpacerWMax() + Image( + painter = painterResource(R.drawable.ic_alert_triangle_20), + contentDescription = null, + modifier = Modifier + .padding( + vertical = TangemTheme.dimens.spacing12, + horizontal = TangemTheme.dimens.spacing14, + ), + ) + } } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index 11c2b5d8de..23af8a4893 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -3,23 +3,19 @@ package com.tangem.features.send.impl.presentation.ui.send import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.core.ui.components.atoms.text.EllipsisText -import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.R @@ -54,48 +50,24 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) .clickable(enabled = !isSuccess) { onClick() } - .padding( - vertical = TangemTheme.dimens.spacing12, - horizontal = TangemTheme.dimens.spacing14, - ), + .padding(TangemTheme.dimens.spacing12), ) { Text( text = stringResource(R.string.common_network_fee_title), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, ) - Row( + SelectorRowItem( + titleRes = title, + iconRes = icon, + preDot = stringReference(feeCryptoValue), + postDot = stringReference(feeFiatValue), + ellipsizeOffset = fee.amount.currencySymbol.length, + isSelected = true, + showDivider = false, + paddingValues = PaddingValues(), modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(icon), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - Text( - text = stringResource(title), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), - ) - EllipsisText( - text = feeCryptoValue, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.End, - ellipsis = TextEllipsis.OffsetEnd(fee.amount.currencySymbol.length), - modifier = Modifier - .weight(1f) - .padding(start = TangemTheme.dimens.spacing4), - ) - Text( - text = "($feeFiatValue)", - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), - ) - } + ) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 4cef8254c1..35f1fb8fe4 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -4,10 +4,11 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -30,7 +31,10 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun ChooseFeeBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet(config) { content: ChooseFeeBottomSheetConfig -> + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.tertiary, + ) { content: ChooseFeeBottomSheetConfig -> ChooseFeeBottomSheetContent(content = content) } } @@ -39,7 +43,7 @@ fun ChooseFeeBottomSheet(config: TangemBottomSheetConfig) { private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) { Column( modifier = Modifier - .background(TangemTheme.colors.background.primary) + .background(TangemTheme.colors.background.tertiary) .padding(bottom = TangemTheme.dimens.spacing8), ) { Text( @@ -53,6 +57,7 @@ private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) { Column( modifier = Modifier .padding(TangemTheme.dimens.spacing16) + .clip(TangemTheme.shapes.roundedCornersXMedium) .background( color = TangemTheme.colors.background.action, shape = TangemTheme.shapes.roundedCornersXMedium, @@ -103,29 +108,36 @@ private fun FooterBlock(readMore: TextReference, readMoreUrl: String, onReadMore @Composable private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { - content.feeItems.forEach { feeItem -> + content.feeItems.forEachIndexed { index, feeItem -> val isSelected = feeItem.feeType == content.selectedFee - val preEllipsizeText = feeItem.amountCrypto - val postEllipsizeText = " ${feeItem.symbolCrypto} (${feeItem.amountFiatFormatted})" + val showDivider = content.feeItems.lastIndex != index + val symbol = " ${feeItem.symbolCrypto}" + val preDotText = "${feeItem.amountCrypto}$symbol" + val postDot = feeItem.amountFiatFormatted + val ellipsizeOffset = symbol.length when (feeItem.feeType) { FeeType.NORMAL -> { SelectorRowItem( titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, - preEllipsize = TextReference.Str(preEllipsizeText), - postEllipsize = TextReference.Str(postEllipsizeText), + preDot = TextReference.Str(preDotText), + postDot = TextReference.Str(postDot), + ellipsizeOffset = ellipsizeOffset, isSelected = isSelected, onSelect = { content.onSelectFeeType(feeItem.feeType) }, + showDivider = showDivider, ) } FeeType.PRIORITY -> { SelectorRowItem( titleRes = R.string.common_fee_selector_option_fast, iconRes = R.drawable.ic_hare_24, - preEllipsize = TextReference.Str(preEllipsizeText), - postEllipsize = TextReference.Str(postEllipsizeText), + preDot = TextReference.Str(preDotText), + postDot = TextReference.Str(postDot), + ellipsizeOffset = ellipsizeOffset, isSelected = isSelected, onSelect = { content.onSelectFeeType(feeItem.feeType) }, + showDivider = showDivider, ) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 6ad6ced2fe..723d81e622 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -2,6 +2,7 @@ package com.tangem.feature.swap.ui import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon @@ -30,7 +31,7 @@ import com.tangem.feature.swap.models.states.PercentDifference import com.tangem.feature.swap.models.states.ProviderState /** - * UI Item for swap provider wrapped in a [BaseContainer] with rounded corners + * UI Item for swap provider wrapped with rounded corners * * https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=7856-41909&mode=design&t=vo7dyElitnzSPSW3-4 */ @@ -43,12 +44,21 @@ private val GrayscaleColorFilter: ColorFilter @Composable fun ProviderItemBlock(state: ProviderState, modifier: Modifier = Modifier) { if (state !is ProviderState.Empty) { - BaseContainer(modifier = modifier) { - ProviderItem( - state = state, - modifier = Modifier.align(Alignment.CenterStart), - ) - } + ProviderItem( + state = state, + modifier = modifier + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .clickable( + enabled = state.onProviderClick != null, + onClick = { state.onProviderClick?.invoke(state.id) }, + ) + .fillMaxWidth() + .padding(vertical = TangemTheme.dimens.spacing12), + ) } } @@ -80,6 +90,7 @@ fun ProviderItem(state: ProviderState, modifier: Modifier = Modifier, isSelected } } +@Deprecated("Replace with InputRowBestRate") @Suppress("LongMethod") @Composable private fun ProviderContentState( @@ -326,22 +337,6 @@ private fun BoxScope.ProviderChevron(selectionType: ProviderState.SelectionType, } } -@Composable -private fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { - Box( - modifier = modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .clip(shape = TangemTheme.shapes.roundedCornersXMedium) - .fillMaxWidth() - .defaultMinSize(minHeight = TangemTheme.dimens.size68), - ) { - content() - } -} - @Composable private fun ErrorProviderIcon(modifier: Modifier = Modifier) { Box( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index fa48e3f6fb..e68875f3ee 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -62,14 +62,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi ) { MainInfo(state) - ProviderItemBlock( - state = state.providerState, - modifier = Modifier - .clickable( - enabled = state.providerState.onProviderClick != null, - onClick = { state.providerState.onProviderClick?.invoke(state.providerState.id) }, - ), - ) + ProviderItemBlock(state = state.providerState) FeeItemBlock(state = state.fee) From 244af774f488ae005ef01f0c9f34f800f7511bad Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Mar 2024 16:34:17 +0500 Subject: [PATCH 045/109] Updated on 2026-08-14 --- .../presentation/state/SendNotification.kt | 54 +++++++- .../impl/presentation/state/SendUiState.kt | 5 +- .../state/amount/AmountNotificationFactory.kt | 38 ++++++ .../state/amount/AmountStateFactory.kt | 9 ++ .../state/amount/SendAmountStateConverter.kt | 2 + .../{ => confirm}/SendNotificationFactory.kt | 92 ++++++++++++- .../state/fee/FeeNotificationFactory.kt | 103 +------------- .../presentation/state/fee/FeeStateFactory.kt | 7 +- .../state/fee/SendFeeNotification.kt | 88 ------------ .../previewdata/AmountStatePreviewData.kt | 2 + .../presentation/ui/amount/AmountButtons.kt | 109 +++++++++++++++ .../ui/amount/AmountFieldContainer.kt | 74 +++++----- .../ui/amount/SendAmountContent.kt | 128 +++++------------- .../ui/fee/SendSpeedAndFeeContent.kt | 10 +- .../presentation/ui/fee/SendSpeedSelector.kt | 5 +- .../impl/presentation/ui/send/SendContent.kt | 8 +- .../presentation/viewmodel/SendViewModel.kt | 28 +++- 17 files changed, 424 insertions(+), 338 deletions(-) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountNotificationFactory.kt rename features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/{ => confirm}/SendNotificationFactory.kt (74%) delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index 64817ff69e..752a235ce0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -11,13 +11,14 @@ internal sealed class SendNotification(val config: NotificationConfig) { sealed class Error( title: TextReference, subtitle: TextReference, + iconResId: Int = R.drawable.ic_alert_24, buttonState: NotificationConfig.ButtonsState? = null, onCloseClick: (() -> Unit)? = null, ) : SendNotification( config = NotificationConfig( title = title, subtitle = subtitle, - iconResId = R.drawable.ic_alert_24, + iconResId = iconResId, buttonsState = buttonState, onCloseClick = onCloseClick, ), @@ -59,6 +60,41 @@ internal sealed class SendNotification(val config: NotificationConfig) { onClick = onConfirmClick, ), ) + + data class ExceedsBalance( + val networkIconId: Int, + val currencyName: String, + val feeName: String, + val feeSymbol: String, + val networkName: String, + val mergeFeeNetworkName: Boolean = false, + val onClick: (() -> Unit)? = null, + ) : Error( + title = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_title, + wrappedList(feeName), + ), + subtitle = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_message, + formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol), + ), + iconResId = networkIconId, + buttonState = onClick?.let { + NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference( + R.string.common_buy_currency, + wrappedList( + if (mergeFeeNetworkName) { + "$currencyName ($feeSymbol)" + } else { + feeName + }, + ), + ), + onClick = onClick, + ) + }, + ) } sealed class Warning( @@ -98,5 +134,21 @@ internal sealed class SendNotification(val config: NotificationConfig) { title = resourceReference(id = R.string.send_notification_transaction_delay_title), subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text), ) + + data class TooHigh( + val value: String, + ) : Warning( + title = resourceReference(id = R.string.send_notification_fee_too_high_title), + subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)), + ) + + data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning( + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference(R.string.send_fee_unreachable_error_text), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_button_refresh), + onClick = onRefresh, + ), + ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index 5ac0949edf..bb10ce4ab4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -10,7 +10,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState -import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.ImmutableList @@ -50,6 +49,8 @@ internal sealed class SendStates { val tokenIconState: TokenIconState, val segmentedButtonConfig: PersistentList, val amountTextField: SendTextField.AmountField, + val notifications: ImmutableList, + val isFeeLoading: Boolean, ) : SendStates() /** Recipient state */ @@ -75,7 +76,7 @@ internal sealed class SendStates { val rate: BigDecimal?, val appCurrency: AppCurrency, val isFeeApproximate: Boolean, - val notifications: ImmutableList, + val notifications: ImmutableList, ) : SendStates() /** Send state */ diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountNotificationFactory.kt new file mode 100644 index 0000000000..222a8704cf --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountNotificationFactory.kt @@ -0,0 +1,38 @@ +package com.tangem.features.send.impl.presentation.state.amount + +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendUiStateType +import com.tangem.features.send.impl.presentation.state.StateRouter +import com.tangem.features.send.impl.presentation.state.SendNotification +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map + +internal class AmountNotificationFactory( + private val stateRouterProvider: Provider, + private val currentStateProvider: Provider, + private val clickIntents: SendClickIntents, +) { + + fun create() = stateRouterProvider().currentState + .filter { it.type == SendUiStateType.Amount } + .map { + buildList { + addFeeUnreachableNotification() + }.toImmutableList() + } + + private fun MutableList.addFeeUnreachableNotification() { + val state = currentStateProvider() + val feeState = state.feeState ?: return + + if (feeState.feeSelectorState is FeeSelectorState.Error) { + add( + SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload), + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt index 6f56b6aff3..626da0c89b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt @@ -1,10 +1,12 @@ package com.tangem.features.send.impl.presentation.state.amount import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldMaxAmountConverter import com.tangem.utils.Provider +import kotlinx.collections.immutable.ImmutableList /** * Factory to produce amount state for [SendUiState] @@ -41,4 +43,11 @@ internal class AmountStateFactory( } fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat) + + fun getAmountNotificationState(notifications: ImmutableList): SendUiState { + val state = currentStateProvider() + return state.copy( + amountState = state.amountState?.copy(notifications = notifications), + ) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt index e1c163e2d4..9e393e8bae 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt @@ -38,6 +38,8 @@ internal class SendAmountStateConverter( tokenIconState = iconStateConverter.convert(status), amountTextField = sendAmountFieldConverter.convert(value), isPrimaryButtonEnabled = false, + notifications = persistentListOf(), + isFeeLoading = false, segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) { persistentListOf() } else { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt similarity index 74% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt rename to features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 0064f20684..85402bfc8f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -1,15 +1,26 @@ -package com.tangem.features.send.impl.presentation.state +package com.tangem.features.send.impl.presentation.state.confirm import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents +import com.tangem.features.send.impl.presentation.state.* +import com.tangem.features.send.impl.presentation.state.SendNotification +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents @@ -33,6 +44,7 @@ internal class SendNotificationFactory( private val stateRouterProvider: Provider, private val clickIntents: SendClickIntents, private val analyticsEventHandler: AnalyticsEventHandler, + private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, ) { fun create(): Flow> = stateRouterProvider().currentState @@ -47,6 +59,7 @@ internal class SendNotificationFactory( buildList { // errors addExceedBalanceNotification(feeAmount, sendAmount) + addExceedsBalanceNotification(feeState.fee) addInvalidAmountNotification(sendState.isSubtract, sendAmount) addMinimumAmountErrorNotification(feeAmount, sendAmount) addDustWarningNotification(feeAmount, sendAmount) @@ -272,6 +285,83 @@ internal class SendNotificationFactory( } } + private suspend fun MutableList.addExceedsBalanceNotification(fee: Fee?) { + val feeValue = fee?.amount?.value ?: BigDecimal.ZERO + val userWalletId = userWalletProvider().walletId + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + + val warning = getBalanceNotEnoughForFeeWarningUseCase( + fee = feeValue, + userWalletId = userWalletId, + tokenStatus = cryptoCurrencyStatus, + coinStatus = coinCryptoCurrencyStatusProvider(), + ).fold( + ifLeft = { null }, + ifRight = { it }, + ) ?: return + + val mergeFeeNetworkName = cryptoCurrencyStatus.shouldMergeFeeNetworkName() + when (warning) { + is CryptoCurrencyWarning.BalanceNotEnoughForFee -> { + add( + SendNotification.Error.ExceedsBalance( + networkIconId = warning.coinCurrency.networkIconResId, + networkName = warning.coinCurrency.name, + currencyName = cryptoCurrencyStatus.currency.name, + feeName = warning.coinCurrency.name, + feeSymbol = warning.coinCurrency.symbol, + mergeFeeNetworkName = mergeFeeNetworkName, + onClick = { + clickIntents.onTokenDetailsClick( + userWalletId = userWalletId, + currency = warning.coinCurrency, + ) + }, + ), + ) + analyticsEventHandler.send( + SendAnalyticEvents.NoticeNotEnoughFee( + token = cryptoCurrencyStatus.currency.symbol, + blockchain = cryptoCurrencyStatus.currency.network.name, + ), + ) + } + is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { + val currency = warning.feeCurrency + add( + SendNotification.Error.ExceedsBalance( + networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24, + currencyName = warning.currency.name, + feeName = warning.feeCurrencyName, + feeSymbol = warning.feeCurrencySymbol, + networkName = warning.networkName, + mergeFeeNetworkName = mergeFeeNetworkName, + onClick = currency?.let { + { + clickIntents.onTokenDetailsClick( + userWalletId, + currency, + ) + } + }, + ), + ) + analyticsEventHandler.send( + SendAnalyticEvents.NoticeNotEnoughFee( + token = warning.currency.symbol, + blockchain = warning.networkName, + ), + ) + } + else -> Unit + } + } + + // workaround for networks that users have misunderstanding + private fun CryptoCurrencyStatus.shouldMergeFeeNetworkName(): Boolean { + return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum + } + companion object { private const val CARDANO_MINIMUM = "1" private const val DOGECOIN_MINIMUM = "0.01" diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt index d3fcad540b..aee69c85fb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt @@ -1,22 +1,12 @@ package com.tangem.features.send.impl.presentation.state.fee -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import com.tangem.utils.toFormattedString @@ -28,14 +18,9 @@ import java.math.BigDecimal @Suppress("LongParameterList") internal class FeeNotificationFactory( - private val coinCryptoCurrencyStatusProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, private val currentStateProvider: Provider, - private val userWalletProvider: Provider, private val stateRouterProvider: Provider, private val clickIntents: SendClickIntents, - private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, ) { fun create() = stateRouterProvider().currentState @@ -53,19 +38,18 @@ internal class FeeNotificationFactory( val customFee = feeSelectorState.customValues val selectedFee = feeSelectorState.selectedFee addTooHighNotification(feeSelectorState.fees, selectedFee, customFee) - addExceedsBalanceNotification(feeState.fee) } } }.toImmutableList() } - private fun MutableList.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) { + private fun MutableList.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) { if (feeSelectorState is FeeSelectorState.Error) { - add(SendFeeNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload)) + add(SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload)) } } - private fun MutableList.addTooHighNotification( + private fun MutableList.addTooHighNotification( transactionFee: TransactionFee, selectedFee: FeeType, customFee: List, @@ -76,87 +60,10 @@ internal class FeeNotificationFactory( val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) val diff = customValue / highValue if (selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF) { - add(SendFeeNotification.Warning.TooHigh(diff.toFormattedString(HIGH_FEE_DIFF_DECIMALS))) + add(SendNotification.Warning.TooHigh(diff.toFormattedString(HIGH_FEE_DIFF_DECIMALS))) } } - private suspend fun MutableList.addExceedsBalanceNotification(fee: Fee?) { - val feeValue = fee?.amount?.value ?: BigDecimal.ZERO - val userWalletId = userWalletProvider().walletId - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - - val warning = getBalanceNotEnoughForFeeWarningUseCase( - fee = feeValue, - userWalletId = userWalletId, - tokenStatus = cryptoCurrencyStatus, - coinStatus = coinCryptoCurrencyStatusProvider(), - ).fold( - ifLeft = { null }, - ifRight = { it }, - ) ?: return - - val mergeFeeNetworkName = cryptoCurrencyStatus.shouldMergeFeeNetworkName() - when (warning) { - is CryptoCurrencyWarning.BalanceNotEnoughForFee -> { - add( - SendFeeNotification.Error.ExceedsBalance( - networkIconId = warning.coinCurrency.networkIconResId, - networkName = warning.coinCurrency.name, - currencyName = cryptoCurrencyStatus.currency.name, - feeName = warning.coinCurrency.name, - feeSymbol = warning.coinCurrency.symbol, - mergeFeeNetworkName = mergeFeeNetworkName, - onClick = { - clickIntents.onTokenDetailsClick( - userWalletId = userWalletId, - currency = warning.coinCurrency, - ) - }, - ), - ) - analyticsEventHandler.send( - SendAnalyticEvents.NoticeNotEnoughFee( - token = cryptoCurrencyStatus.currency.symbol, - blockchain = cryptoCurrencyStatus.currency.network.name, - ), - ) - } - is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { - val currency = warning.feeCurrency - add( - SendFeeNotification.Error.ExceedsBalance( - networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24, - currencyName = warning.currency.name, - feeName = warning.feeCurrencyName, - feeSymbol = warning.feeCurrencySymbol, - networkName = warning.networkName, - mergeFeeNetworkName = mergeFeeNetworkName, - onClick = currency?.let { - { - clickIntents.onTokenDetailsClick( - userWalletId, - currency, - ) - } - }, - ), - ) - analyticsEventHandler.send( - SendAnalyticEvents.NoticeNotEnoughFee( - token = warning.currency.symbol, - blockchain = warning.networkName, - ), - ) - } - else -> Unit - } - } - - // workaround for networks that users have misunderstanding - private fun CryptoCurrencyStatus.shouldMergeFeeNetworkName(): Boolean { - return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum - } - companion object { private val FEE_MAX_DIFF = BigDecimal(5) private const val HIGH_FEE_DIFF_DECIMALS = 0 diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index 6a2804103f..d2339a3de9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase +import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents @@ -111,7 +112,7 @@ internal class FeeStateFactory( ) } - fun getFeeNotificationState(notifications: ImmutableList): SendUiState { + fun getFeeNotificationState(notifications: ImmutableList): SendUiState { val state = currentStateProvider() return state.copy( feeState = state.feeState?.copy( @@ -123,7 +124,7 @@ internal class FeeStateFactory( private fun isPrimaryButtonEnabled( feeState: SendStates.FeeState, - notifications: ImmutableList, + notifications: ImmutableList, ): Boolean { val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return false val customValue = feeSelectorState.customValues.firstOrNull() @@ -134,7 +135,7 @@ internal class FeeStateFactory( } else { false } - val noErrors = notifications.none { it is SendFeeNotification.Error } + val noErrors = notifications.none { it is SendNotification.Error } return noErrors && (isNotEmptyCustom || isNotCustom) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt deleted file mode 100644 index b3a2846e5f..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.tangem.features.send.impl.presentation.state.fee - -import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.features.send.impl.R - -sealed class SendFeeNotification(val config: NotificationConfig) { - - sealed class Warning( - val title: TextReference, - val subtitle: TextReference, - val buttonsState: NotificationConfig.ButtonsState? = null, - ) : SendFeeNotification( - config = NotificationConfig( - title = title, - subtitle = subtitle, - iconResId = R.drawable.img_attention_20, - buttonsState = buttonsState, - ), - ) { - data class TooHigh( - val value: String, - ) : Warning( - title = resourceReference(id = R.string.send_notification_fee_too_high_title), - subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)), - ) - - data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning( - title = resourceReference(R.string.send_fee_unreachable_error_title), - subtitle = resourceReference(R.string.send_fee_unreachable_error_text), - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.warning_button_refresh), - onClick = onRefresh, - ), - ) - } - - sealed class Error( - val title: TextReference, - val subtitle: TextReference, - val iconResId: Int, - val buttonsState: NotificationConfig.ButtonsState? = null, - ) : SendFeeNotification( - config = NotificationConfig( - title = title, - subtitle = subtitle, - iconResId = iconResId, - buttonsState = buttonsState, - ), - ) { - data class ExceedsBalance( - val networkIconId: Int, - val currencyName: String, - val feeName: String, - val feeSymbol: String, - val networkName: String, - val mergeFeeNetworkName: Boolean = false, - val onClick: (() -> Unit)? = null, - ) : Error( - title = resourceReference( - id = R.string.warning_send_blocked_funds_for_fee_title, - wrappedList(feeName), - ), - subtitle = resourceReference( - id = R.string.warning_send_blocked_funds_for_fee_message, - formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol), - ), - iconResId = networkIconId, - buttonsState = onClick?.let { - NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference( - R.string.common_buy_currency, - wrappedList( - if (mergeFeeNetworkName) { - "$currencyName ($feeSymbol)" - } else { - feeName - }, - ), - ), - onClick = onClick, - ) - }, - ) - } -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt index 8423ec20eb..43d0d75572 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt @@ -22,6 +22,8 @@ internal object AmountStatePreviewData { walletBalance = stringReference("123.123"), tokenIconState = TokenIconState.Loading, segmentedButtonConfig = persistentListOf(), + notifications = persistentListOf(), + isFeeLoading = false, amountTextField = SendTextField.AmountField( value = "123.123123123123123123", onValueChange = {}, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt new file mode 100644 index 0000000000..68573bf3c7 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt @@ -0,0 +1,109 @@ +package com.tangem.features.send.impl.presentation.ui.amount + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.components.currency.fiaticon.FiatIcon +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import kotlinx.collections.immutable.PersistentList + +private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey" + +internal fun LazyListScope.buttons( + segmentedButtonConfig: PersistentList, + clickIntents: SendClickIntents, +) { + item( + key = AMOUNT_BUTTONS_KEY, + ) { + val hapticFeedback = LocalHapticFeedback.current + Row( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + ) { + if (segmentedButtonConfig.isNotEmpty()) { + SegmentedButtons( + modifier = Modifier + .weight(1f) + .height(TangemTheme.dimens.size40), + config = segmentedButtonConfig, + showIndication = false, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + clickIntents.onCurrencyChangeClick(it.isFiat) + }, + ) { + SendAmountCurrencyButton(it) + } + } else { + SpacerWMax() + } + SecondaryButton( + text = stringResource(R.string.send_max_amount), + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + clickIntents.onMaxValueClick() + }, + size = TangemButtonSize.Text, + shape = RoundedCornerShape(TangemTheme.dimens.radius26), + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8) + .height(TangemTheme.dimens.size40), + ) + } + } +} + +@Composable +private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) { + Row( + modifier = Modifier + .fillMaxSize() + .padding( + horizontal = TangemTheme.dimens.spacing10, + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + if (button.isFiat) { + FiatIcon( + url = button.iconUrl, + size = TangemTheme.dimens.size18, + modifier = Modifier.size(TangemTheme.dimens.size18), + ) + } else { + button.iconState?.let { + TokenIcon( + state = it, + shouldDisplayNetwork = false, + modifier = Modifier + .size(TangemTheme.dimens.size18), + ) + } + } + Text( + text = button.title.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.button, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing8, + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt index bc0887866a..9b44cc2e1f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt @@ -5,9 +5,9 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text -import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -18,50 +18,52 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendStates -@Composable -internal fun AmountFieldContainer( +private const val AMOUNT_FIELD_KEY = "amountFieldKey" + +internal fun LazyListScope.amountField( amountState: SendStates.AmountState, isBalanceHiding: Boolean, modifier: Modifier = Modifier, ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing16) - .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action), - ) { - Text( - text = amountState.walletName, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing14), - ) - - val balance = if (isBalanceHiding) STARS else amountState.walletBalance.resolveReference() - AnimatedContent( - targetState = balance, - label = "Hide Balance Animation", + item(key = AMOUNT_FIELD_KEY) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.background.action), ) { Text( - text = it, - style = TangemTheme.typography.caption2, + text = amountState.walletName, + style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing2), + .padding(top = TangemTheme.dimens.spacing14), + ) + + val balance = if (isBalanceHiding) STARS else amountState.walletBalance.resolveReference() + AnimatedContent( + targetState = balance, + label = "Hide Balance Animation", + ) { + Text( + text = it, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing2), + ) + } + TokenIcon( + state = amountState.tokenIconState, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing32), + ) + AmountField( + sendField = amountState.amountTextField, + isFiat = amountState.amountTextField.isFiatValue, ) } - TokenIcon( - state = amountState.tokenIconState, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing32), - ) - AmountField( - sendField = amountState.amountTextField, - isFiat = amountState.amountTextField.isFiatValue, - ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt index 143b2e1e16..3f3d18d3bd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt @@ -1,27 +1,19 @@ package com.tangem.features.send.impl.presentation.ui.amount +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.components.buttons.common.TangemButtonSize -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.currency.fiaticon.FiatIcon -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon -import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendStates -import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import kotlinx.collections.immutable.ImmutableList @Composable internal fun SendAmountContent( @@ -30,88 +22,38 @@ internal fun SendAmountContent( clickIntents: SendClickIntents, ) { if (amountState == null) return - val hapticFeedback = LocalHapticFeedback.current - Column( + LazyColumn( modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) .background(TangemTheme.colors.background.tertiary), ) { - AmountFieldContainer(amountState = amountState, isBalanceHiding = isBalanceHiding) - Row( - modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing12, - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - ), - ) { - if (amountState.segmentedButtonConfig.isNotEmpty()) { - SegmentedButtons( - modifier = Modifier - .weight(1f) - .height(TangemTheme.dimens.size40), - config = amountState.segmentedButtonConfig, - showIndication = false, - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - clickIntents.onCurrencyChangeClick(it.isFiat) - }, - ) { - SendAmountCurrencyButton(it) - } - } else { - SpacerWMax() - } - SecondaryButton( - text = stringResource(R.string.send_max_amount), - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - clickIntents.onMaxValueClick() - }, - size = TangemButtonSize.Text, - shape = RoundedCornerShape(TangemTheme.dimens.radius26), - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing8) - .height(TangemTheme.dimens.size40), - ) - } + amountField(amountState = amountState, isBalanceHiding = isBalanceHiding) + buttons(amountState.segmentedButtonConfig, clickIntents) + notifications(amountState.notifications) } } -@Composable -private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) { - Row( - modifier = Modifier - .fillMaxSize() - .padding( - horizontal = TangemTheme.dimens.spacing10, - ), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - if (button.isFiat) { - FiatIcon( - url = button.iconUrl, - size = TangemTheme.dimens.size18, - modifier = Modifier.size(TangemTheme.dimens.size18), +@OptIn(ExperimentalFoundationApi::class) +internal fun LazyListScope.notifications(configs: ImmutableList, modifier: Modifier = Modifier) { + items( + items = configs, + key = { it::class.java }, + contentType = { it::class.java }, + itemContent = { + val bottomPadding = if (it == configs.last()) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing0 + Notification( + config = it.config, + modifier = modifier + .animateItemPlacement() + .padding( + top = TangemTheme.dimens.spacing12, + bottom = bottomPadding, + ), + containerColor = when (it) { + is SendNotification.Warning.NetworkFeeUnreachable -> TangemTheme.colors.background.action + else -> TangemTheme.colors.button.disabled + }, ) - } else { - button.iconState?.let { - TokenIcon( - state = it, - shouldDisplayNetwork = false, - modifier = Modifier - .size(TangemTheme.dimens.size18), - ) - } - } - Text( - text = button.title.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.button, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing8, - ), - ) - } + }, + ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt index 444d03e9a6..f9e410b2ee 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -13,9 +13,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState -import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.ImmutableList @@ -56,7 +56,7 @@ private fun LazyListScope.feeSelector(state: SendStates.FeeState, clickIntents: @OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.notifications( - configs: ImmutableList, + configs: ImmutableList, modifier: Modifier = Modifier, isLast: Boolean = false, ) { @@ -75,13 +75,13 @@ private fun LazyListScope.notifications( ) .animateItemPlacement(), containerColor = when (it) { - is SendFeeNotification.Error.ExceedsBalance, - is SendFeeNotification.Warning.NetworkFeeUnreachable, + is SendNotification.Error.ExceedsBalance, + is SendNotification.Warning.NetworkFeeUnreachable, -> TangemTheme.colors.background.action else -> TangemTheme.colors.button.disabled }, iconTint = when (it) { - is SendFeeNotification.Error.ExceedsBalance -> { + is SendNotification.Error.ExceedsBalance -> { if (it.config.buttonsState == null) { TangemTheme.colors.icon.warning } else { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index e0a3868ec1..dfaed7cb57 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.text.ClickableText 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.Modifier import androidx.compose.ui.draw.clip @@ -40,10 +39,10 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.BigDecimalFormatter.CAN_BE_LOWER_SIGN import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType -import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import java.math.BigDecimal @@ -113,7 +112,7 @@ internal fun SendSpeedSelector( visible = fees.normal is Fee.Ethereum, label = "Custom fee appearance animation", ) { - val showWarning = state.notifications.any { it is SendFeeNotification.Warning.TooHigh } + val showWarning = state.notifications.any { it is SendNotification.Warning.TooHigh } SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_custom, iconRes = R.drawable.ic_edit_24, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 9dff71b1db..c5be1f7b27 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -146,12 +146,16 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.background.action + is SendNotification.Error.ExceedsBalance, + is SendNotification.Warning.HighFeeError, + -> TangemTheme.colors.background.action else -> TangemTheme.colors.button.disabled }, iconTint = when (it) { + is SendNotification.Error.ExceedsBalance, + is SendNotification.Warning, + -> null is SendNotification.Error -> TangemTheme.colors.icon.warning - is SendNotification.Warning -> null }, ) }, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index bc74ced905..fd799169cc 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -47,7 +47,9 @@ import com.tangem.features.send.impl.presentation.analytics.SendScreenSource import com.tangem.features.send.impl.presentation.analytics.utils.SendOnNextScreenAnalyticSender import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.* +import com.tangem.features.send.impl.presentation.state.amount.AmountNotificationFactory import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactory +import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory import com.tangem.features.send.impl.presentation.state.fee.* import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider @@ -146,15 +148,16 @@ internal class SendViewModel @Inject constructor( feeStateFactory = feeStateFactory, ) - private val feeNotificationFactory = FeeNotificationFactory( - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, + private val amountNotificationFactory = AmountNotificationFactory( + currentStateProvider = Provider { uiState }, + stateRouterProvider = Provider { stateRouter }, + clickIntents = this, + ) + + private val feeNotificationFactory = FeeNotificationFactory( currentStateProvider = Provider { uiState }, - userWalletProvider = Provider { userWallet }, stateRouterProvider = Provider { stateRouter }, clickIntents = this, - getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase, - analyticsEventHandler = analyticsEventHandler, ) private val sendNotificationFactory = SendNotificationFactory( @@ -166,6 +169,7 @@ internal class SendViewModel @Inject constructor( currencyChecksRepository = currencyChecksRepository, clickIntents = this, analyticsEventHandler = analyticsEventHandler, + getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase, ) private val sendOnNextScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) { @@ -191,6 +195,7 @@ internal class SendViewModel @Inject constructor( private var memoValidationJobHolder = JobHolder() private var sendNotificationsJobHolder = JobHolder() private var feeNotificationsJobHolder = JobHolder() + private var amountNotificationsJobHolder = JobHolder() private var qrScannerJobHolder = JobHolder() private var sendIdleTimer = 0L @@ -465,6 +470,16 @@ internal class SendViewModel @Inject constructor( .saveIn(feeNotificationsJobHolder) } + private fun updateAmountNotifications() { + amountNotificationFactory.create() + .conflate() + .distinctUntilChanged() + .onEach { uiState = amountStateFactory.getAmountNotificationState(notifications = it) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(amountNotificationsJobHolder) + } + // region screen state navigation override fun popBackStack() = stateRouter.popBackStack() override fun onBackClick() = stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true) @@ -643,6 +658,7 @@ internal class SendViewModel @Inject constructor( ifLeft = { feeStateFactory.onFeeOnErrorState() }, ) ?: feeStateFactory.onFeeOnErrorState() updateFeeNotifications() + updateAmountNotifications() }.saveIn(feeJobHolder) } From 23db294dc5994758785725c6afef08939193c967 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Mar 2024 14:49:34 +0300 Subject: [PATCH 046/109] Updated on 2026-08-14 --- .../tokendetails/ui/components/exchange/ExchangeProvider.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt index 173e79dc30..5f3527a8eb 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange +import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -11,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -30,6 +32,7 @@ internal fun ExchangeProvider( ) { val clipboardManager = LocalClipboardManager.current val hapticFeedback = LocalHapticFeedback.current + val context = LocalContext.current Column( modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) @@ -54,6 +57,7 @@ internal fun ExchangeProvider( .clickable { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) clipboardManager.setText(AnnotatedString(providerTxId)) + Toast.makeText(context, R.string.express_transaction_id_copied, Toast.LENGTH_SHORT).show() }, ) { Icon( From 544d99b169bd3401c74e39e0dd3922a74f434490 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Mar 2024 19:56:29 +0000 Subject: [PATCH 047/109] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 1 - .../src/main/res/values-zh-rTW/strings.xml | 1 - core/res/src/main/res/values/strings.xml | 1 - .../segmentedbutton/SegmentedButton.kt | 6 +- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../data/DefaultMnemonicRepository.kt | 8 + .../onboarding/data/MnemonicRepository.kt | 4 + .../domain/DefaultSeedPhraseInteractor.kt | 15 +- .../onboarding/domain/SeedPhraseInteractor.kt | 3 +- .../onboarding/domain/models/MnemonicType.kt | 5 + .../model/OnboardingSeedPhraseState.kt | 36 +++- .../presentation/wallet2/model/UiActions.kt | 1 + .../wallet2/ui/YourSeedPhraseScreen.kt | 197 ++++++++++++------ .../ui/components/OnboardingActionBlock.kt | 3 +- .../components/OnboardingDescriptionBlock.kt | 19 +- .../wallet2/ui/stateBuiders/StateBuilder.kt | 149 +++++++++---- .../wallet2/viewmodel/SeedPhraseViewModel.kt | 96 +++++---- 17 files changed, 369 insertions(+), 177 deletions(-) create mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/models/MnemonicType.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 95cd7e9498..41a397e283 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -313,7 +313,6 @@ Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа? Резервная копия Прочитать о seed-фразе - Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте. Запишите эти %d слова в порядке, указанном ниже, и сохраните их в надежном месте. diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 841f0df3bb..e0b8c69381 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -174,7 +174,6 @@ 您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎? 創建備份 閱讀更多關於助記詞的訊息 - 按照下面給出的順序寫下這 12 個單詞,並將它們存放在安全秘密的地方。 按照下面給出的順序寫下這 %d 個單詞,並將它們存放在安全秘密的地方。 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 597b184e40..11cf3d5316 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -313,7 +313,6 @@ Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet? Creating a backup Read more about seed phrase - Write these 12 words down in the order given below and store them in a safe and secret place. Write these %d words down in the order given below and store them in a safe and secret place. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt index 44d622f9eb..712fc8cb52 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt @@ -47,11 +47,15 @@ inline fun SegmentedButtons( selectedColor: Color = TangemTheme.colors.background.action, dividerColor: Color = TangemTheme.colors.stroke.primary, showIndication: Boolean = true, + selectedItem: T? = null, crossinline buttonContent: @Composable (T) -> Unit, ) { if (config.isEmpty() || config.size == 1) return - var selected by remember { mutableIntStateOf(0) } + var selected by remember { + val selectedIndex = if (selectedItem == null) 0 else config.indexOf(selectedItem) + mutableIntStateOf(selectedIndex) + } Row( modifier = modifier diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 13ec4eb908..d835895565 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -125,6 +125,7 @@ data class TangemDimens internal constructor( val spacing56: Dp = 56.dp, val spacing60: Dp = 60.dp, val spacing70: Dp = 70.dp, + val spacing76: Dp = 76.dp, val spacing80: Dp = 80.dp, val spacing92: Dp = 92.dp, val spacing94: Dp = 94.dp, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt index 71ac3393b7..09bc1f8964 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt @@ -4,6 +4,7 @@ import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.bip39.EntropyLength import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.bip39.Wordlist +import com.tangem.feature.onboarding.domain.models.MnemonicType /** [REDACTED_AUTHOR] @@ -20,5 +21,12 @@ class DefaultMnemonicRepository( return DefaultMnemonic(EntropyLength.Bits128Length, bip39Wordlist) } + override fun generateMnemonic(mnemonicType: MnemonicType): Mnemonic { + return when (mnemonicType) { + MnemonicType.Mnemonic12 -> DefaultMnemonic(EntropyLength.Bits128Length, bip39Wordlist) + MnemonicType.Mnemonic24 -> DefaultMnemonic(EntropyLength.Bits256Length, bip39Wordlist) + } + } + override fun createMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(mnemonicString, bip39Wordlist) } \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt index b1b7ebfa2c..389ca4d949 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt @@ -2,6 +2,7 @@ package com.tangem.feature.onboarding.data import com.tangem.common.core.TangemSdkError import com.tangem.crypto.bip39.Mnemonic +import com.tangem.feature.onboarding.domain.models.MnemonicType /** [REDACTED_AUTHOR] @@ -13,6 +14,9 @@ interface MnemonicRepository { @Throws(TangemSdkError.MnemonicException::class) fun generateDefaultMnemonic(): Mnemonic + @Throws(TangemSdkError.MnemonicException::class) + fun generateMnemonic(mnemonicType: MnemonicType): Mnemonic + @Throws(TangemSdkError.MnemonicException::class) fun createMnemonic(mnemonicString: String): Mnemonic } \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt index 92ed7ae69c..a0ef98bf30 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt @@ -4,6 +4,7 @@ import com.tangem.common.core.TangemSdkError import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.bip39.MnemonicErrorResult import com.tangem.feature.onboarding.data.MnemonicRepository +import com.tangem.feature.onboarding.domain.models.MnemonicType import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField import com.tangem.utils.extensions.isNotWhitespace import kotlinx.collections.immutable.ImmutableList @@ -13,27 +14,23 @@ import kotlinx.collections.immutable.toPersistentList /** [REDACTED_AUTHOR] */ -internal class DefaultSeedPhraseInteractor constructor( +internal class DefaultSeedPhraseInteractor( private val repository: MnemonicRepository, ) : SeedPhraseInteractor { - private var currentMnemonic: Mnemonic? = null private val partWordFinder: PartWordFinder = PartWordFinder() override suspend fun generateMnemonic(): Result { return runCatching { - repository.generateDefaultMnemonic().apply { - currentMnemonic = this - } + repository.generateDefaultMnemonic() } } - override suspend fun getMnemonicComponents(): Result> { + override suspend fun generateMnemonics(mnemonicTypes: List): Result> { return runCatching { - val mnemonic = currentMnemonic ?: repository.generateDefaultMnemonic().apply { - currentMnemonic = this + mnemonicTypes.associateWith { + repository.generateMnemonic(it) } - mnemonic.mnemonicComponents } } diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt index d9420a9def..37e0c182da 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt @@ -1,6 +1,7 @@ package com.tangem.feature.onboarding.domain import com.tangem.crypto.bip39.Mnemonic +import com.tangem.feature.onboarding.domain.models.MnemonicType import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField import kotlinx.collections.immutable.ImmutableList @@ -9,7 +10,7 @@ import kotlinx.collections.immutable.ImmutableList */ interface SeedPhraseInteractor { suspend fun generateMnemonic(): Result - suspend fun getMnemonicComponents(): Result> + suspend fun generateMnemonics(mnemonicTypes: List): Result> suspend fun isWordMatch(mnemonicComponents: List?, field: SeedPhraseField, word: String): Boolean suspend fun validateMnemonicString(text: String): Result> suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): ImmutableList diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/models/MnemonicType.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/models/MnemonicType.kt new file mode 100644 index 0000000000..ea4a74f87f --- /dev/null +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/models/MnemonicType.kt @@ -0,0 +1,5 @@ +package com.tangem.feature.onboarding.domain.models + +enum class MnemonicType { + Mnemonic12, Mnemonic24; +} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt index 44bdac25c3..5591aa1013 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt @@ -2,7 +2,9 @@ package com.tangem.feature.onboarding.presentation.wallet2.model import androidx.compose.ui.text.input.TextFieldValue import com.tangem.feature.onboarding.domain.SeedPhraseError +import com.tangem.feature.onboarding.domain.models.MnemonicType import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf /** @@ -31,10 +33,42 @@ data class AboutState( ) data class YourSeedPhraseState( - val mnemonicGridItems: ImmutableList = persistentListOf(), + val segmentSeedState: SegmentSeedState, + val mnemonicGridItems: PersistentList = persistentListOf(), val buttonContinue: ButtonState, ) +data class SegmentSeedState( + val seedSegments: PersistentList, + val selectedSeedType: SegmentSeedType, + val onSelectType: (SegmentSeedType) -> Unit, +) + +enum class SegmentSeedType(val count: Int) { + SEED_12(count = 12), SEED_24(count = 24); + + fun toMnemonicType(): MnemonicType { + return when (this) { + SEED_12 -> MnemonicType.Mnemonic12 + SEED_24 -> MnemonicType.Mnemonic24 + } + } + + companion object { + fun fromMnemonicType(mnemonicType: MnemonicType): SegmentSeedType { + return when (mnemonicType) { + MnemonicType.Mnemonic12 -> SEED_12 + MnemonicType.Mnemonic24 -> SEED_24 + } + } + } +} + +data class MnemonicState( + val seedType: SegmentSeedType, + val mnemonicItems: PersistentList, +) + data class MnemonicGridItem( val index: Int, val mnemonic: String, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt index dec0e444b9..afb379bdca 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt @@ -28,6 +28,7 @@ data class AboutUiAction( data class YourSeedPhraseUiAction( val buttonContinueClick: () -> Unit, + val onSelectType: (SegmentSeedType) -> Unit, ) data class CheckSeedPhraseUiAction( diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt index 23e21b9c92..94f072a0ac 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt @@ -2,93 +2,162 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SpacerW32 +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.R -import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState -import com.tangem.feature.onboarding.presentation.wallet2.model.MnemonicGridItem -import com.tangem.feature.onboarding.presentation.wallet2.model.YourSeedPhraseState -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description +import com.tangem.feature.onboarding.presentation.wallet2.model.* +import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText +import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionTitleText import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList /** [REDACTED_AUTHOR] */ @Composable fun YourSeedPhraseScreen(state: YourSeedPhraseState, modifier: Modifier = Modifier) { - Column( + Box( modifier = modifier.fillMaxSize(), ) { - Box( - modifier = Modifier.weight(weight = 0.6f), + Column( + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing92) + .verticalScroll(rememberScrollState()) + .fillMaxSize(), ) { - OnboardingDescriptionBlock( - modifier = Modifier.align(Alignment.Center), - ) { - Description( - titleRes = R.string.onboarding_seed_generate_title, - subTitleRes = R.string.onboarding_seed_generate_message, - ) - } - } - Box( - modifier = Modifier.weight(weight = 1f), - ) { - PhraseGreedBlock(state.mnemonicGridItems) - } + SegmentSeedBlock( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing20), + state = state.segmentSeedState, + ) - Box { - OnboardingActionBlock( - firstActionContent = { - PrimaryButton( - modifier = Modifier - .fillMaxWidth(), - text = stringResource(id = R.string.common_continue), - onClick = state.buttonContinue.onClick, - ) - }, + TitleBlock(state.segmentSeedState) + + SeedPhraseGridBlock( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing20) + .padding(horizontal = TangemTheme.dimens.size16), + mnemonicGridItems = state.mnemonicGridItems, + ) + } + OnboardingActionBlock( + modifier = Modifier.align(Alignment.BottomCenter), + firstActionContent = { + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.common_continue), + onClick = state.buttonContinue.onClick, + ) + }, + ) + } +} + +@Composable +private fun TitleBlock(state: SegmentSeedState) { + Box( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.size36) + .padding(top = TangemTheme.dimens.spacing20) + .fillMaxWidth(), + ) { + Column { + DescriptionTitleText(text = stringResource(id = R.string.onboarding_seed_generate_title)) + SpacerH16() + DescriptionSubTitleText( + text = pluralStringResource( + id = R.plurals.onboarding_seed_generate_message_words_count, + count = state.selectedSeedType.count, + state.selectedSeedType.count, + ), ) } } } @Composable -private fun PhraseGreedBlock(mnemonicGridItems: ImmutableList) { - LazyVerticalGrid( - columns = GridCells.Fixed(count = 2), +private fun SegmentSeedBlock(state: SegmentSeedState, modifier: Modifier = Modifier) { + SegmentedButtons( + modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing76), + config = state.seedSegments, + selectedItem = state.selectedSeedType, + onClick = { + state.onSelectType(it) + }, ) { - this.items( - count = mnemonicGridItems.size, - ) { index -> - Row( - modifier = Modifier.padding(all = TangemTheme.dimens.size8), - verticalAlignment = Alignment.CenterVertically, - ) { - val item = mnemonicGridItems[index] - SpacerW32() - Text( - modifier = Modifier.width(TangemTheme.dimens.size40), - text = "${item.index}.", - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - ) - Text( - text = item.mnemonic, - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary1, - ) + Text( + text = pluralStringResource( + id = R.plurals.onboarding_seed_generate_words_count, + count = it.count, + it.count, + ), + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun SeedPhraseGridBlock(mnemonicGridItems: ImmutableList, modifier: Modifier = Modifier) { + VerticalGrid( + modifier = modifier, + items = mnemonicGridItems, + columns = 2, + ) { item -> + Row( + modifier = Modifier.padding(all = TangemTheme.dimens.size8), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.width(TangemTheme.dimens.size40), + text = "${item.index}.", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + Text( + text = item.mnemonic, + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary1, + ) + } + } +} + +@Composable +private inline fun VerticalGrid( + items: ImmutableList, + columns: Int, + modifier: Modifier = Modifier, + crossinline content: @Composable (T) -> Unit, +) { + val columnLength = items.size / columns + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + for (index in 0 until columns) { + Column { + for (i in 0 until columnLength) { + val item = items[index * columnLength + i] + content(item) + } } } } @@ -100,7 +169,15 @@ private fun YourSeedPhraseScreenPreview_Light() { TangemTheme(isDark = false) { YourSeedPhraseScreen( state = YourSeedPhraseState( - mnemonicGridItems = (1..12).map { MnemonicGridItem(it, it.toString()) }.toImmutableList(), + segmentSeedState = SegmentSeedState( + seedSegments = persistentListOf( + SegmentSeedType.SEED_12, + SegmentSeedType.SEED_24, + ), + selectedSeedType = SegmentSeedType.SEED_24, + onSelectType = {}, + ), + mnemonicGridItems = (1..24).map { MnemonicGridItem(it, it.toString()) }.toPersistentList(), buttonContinue = ButtonState( enabled = true, isClickable = true, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt index b7706cb69e..986bcac496 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt @@ -14,13 +14,14 @@ import com.tangem.core.ui.res.TangemTheme */ @Composable fun OnboardingActionBlock( + modifier: Modifier = Modifier, firstActionContent: @Composable (() -> Unit)? = null, secondActionContent: @Composable (() -> Unit)? = null, ) { if (firstActionContent == null && secondActionContent == null) return Box( - modifier = Modifier + modifier = modifier .padding(horizontal = TangemTheme.dimens.size16) .padding( top = TangemTheme.dimens.size8, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt index 4134269aaa..5dc8a2a1c0 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt @@ -11,10 +11,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp -import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.onboarding.presentation.wallet2.model.DescriptionResource -import kotlinx.collections.immutable.ImmutableList /** * UI component witch provides single and carousel description for the onboarding process @@ -24,7 +22,7 @@ fun OnboardingDescriptionBlock(modifier: Modifier = Modifier, content: @Composab Box( modifier = modifier .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.size24), + .padding(horizontal = TangemTheme.dimens.size36), ) { content() } @@ -34,22 +32,11 @@ fun OnboardingDescriptionBlock(modifier: Modifier = Modifier, content: @Composab fun Description(@StringRes titleRes: Int, @StringRes subTitleRes: Int) { Column { DescriptionTitleText(text = stringResource(id = titleRes)) - SpacerH12() + SpacerH16() DescriptionSubTitleText(text = stringResource(id = subTitleRes)) } } -@Suppress("UnusedPrivateMember") -@Composable -fun OnboardingCarouselDescriptionBlock( - descriptionsList: ImmutableList, - modifier: Modifier = Modifier, -) { - OnboardingDescriptionBlock(modifier) { - // TODO: implement - } -} - @Composable fun DescriptionTitleText(text: String) { Text( diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt index 8683c0c3c0..749d4be6a0 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt @@ -1,7 +1,8 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders import com.tangem.feature.onboarding.presentation.wallet2.model.* -import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf /** [REDACTED_AUTHOR] @@ -13,32 +14,34 @@ class StateBuilder( val checkSeedPhrase: CheckSeedPhraseStateBuilder = CheckSeedPhraseStateBuilder() val importSeedPhrase: ImportSeedPhraseStateBuilder = ImportSeedPhraseStateBuilder() - fun init(): OnboardingSeedPhraseState = OnboardingSeedPhraseState( - introState = IntroState( + fun init(selectedSeedType: SegmentSeedType): OnboardingSeedPhraseState = OnboardingSeedPhraseState( + introState = getInitialIntroState(), + aboutState = getInitialAboutState(), + yourSeedPhraseState = getInitialYourSeedPhraseState(selectedSeedType), + checkSeedPhraseState = getInitialCheckSeedPhraseState(), + importSeedPhraseState = getInitialImportSeedPhraseState(), + menuButtonChat = ButtonState( + onClick = uiActions.menuChatClick, + ), + onBackClick = uiActions.menuNavigateBackClick, + ) + + private fun getInitialImportSeedPhraseState(): ImportSeedPhraseState { + return ImportSeedPhraseState( + tvSeedPhrase = TextFieldState( + onTextFieldValueChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onTextFieldChanged, + onFocusChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onFocusChanged, + ), + onSuggestedPhraseClick = uiActions.importSeedPhraseActions.suggestedPhraseClick, buttonCreateWallet = ButtonState( - onClick = uiActions.introActions.buttonCreateWalletClick, + enabled = false, + onClick = uiActions.importSeedPhraseActions.buttonCreateWalletClick, ), - buttonOtherOptions = ButtonState( - onClick = uiActions.introActions.buttonOtherOptionsClick, - ), - ), - aboutState = AboutState( - buttonReadMoreAboutSeedPhrase = ButtonState( - onClick = uiActions.aboutActions.buttonReadMoreAboutSeedPhraseClick, - ), - buttonGenerateSeedPhrase = ButtonState( - onClick = uiActions.aboutActions.buttonGenerateSeedPhraseClick, - ), - buttonImportSeedPhrase = ButtonState( - onClick = uiActions.aboutActions.buttonImportSeedPhraseClick, - ), - ), - yourSeedPhraseState = YourSeedPhraseState( - buttonContinue = ButtonState( - onClick = uiActions.yourSeedPhraseActions.buttonContinueClick, - ), - ), - checkSeedPhraseState = CheckSeedPhraseState( + ) + } + + private fun getInitialCheckSeedPhraseState(): CheckSeedPhraseState { + return CheckSeedPhraseState( tvSecondPhrase = TextFieldState( label = "2", isFocused = true, @@ -61,23 +64,49 @@ class StateBuilder( enabled = false, onClick = uiActions.checkSeedPhraseActions.buttonCreateWalletClick, ), - ), - importSeedPhraseState = ImportSeedPhraseState( - tvSeedPhrase = TextFieldState( - onTextFieldValueChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onTextFieldChanged, - onFocusChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onFocusChanged, + ) + } + + private fun getInitialYourSeedPhraseState(selectedSeedType: SegmentSeedType): YourSeedPhraseState { + return YourSeedPhraseState( + segmentSeedState = SegmentSeedState( + seedSegments = persistentListOf( + SegmentSeedType.SEED_12, + SegmentSeedType.SEED_24, + ), + selectedSeedType = selectedSeedType, + onSelectType = uiActions.yourSeedPhraseActions.onSelectType, ), - onSuggestedPhraseClick = uiActions.importSeedPhraseActions.suggestedPhraseClick, + buttonContinue = ButtonState( + onClick = uiActions.yourSeedPhraseActions.buttonContinueClick, + ), + ) + } + + private fun getInitialAboutState(): AboutState { + return AboutState( + buttonReadMoreAboutSeedPhrase = ButtonState( + onClick = uiActions.aboutActions.buttonReadMoreAboutSeedPhraseClick, + ), + buttonGenerateSeedPhrase = ButtonState( + onClick = uiActions.aboutActions.buttonGenerateSeedPhraseClick, + ), + buttonImportSeedPhrase = ButtonState( + onClick = uiActions.aboutActions.buttonImportSeedPhraseClick, + ), + ) + } + + private fun getInitialIntroState(): IntroState { + return IntroState( buttonCreateWallet = ButtonState( - enabled = false, - onClick = uiActions.importSeedPhraseActions.buttonCreateWalletClick, + onClick = uiActions.introActions.buttonCreateWalletClick, ), - ), - menuButtonChat = ButtonState( - onClick = uiActions.menuChatClick, - ), - onBackClick = uiActions.menuNavigateBackClick, - ) + buttonOtherOptions = ButtonState( + onClick = uiActions.introActions.buttonOtherOptionsClick, + ), + ) + } fun setCardArtwork(uiState: OnboardingSeedPhraseState, cardArtwork: String): OnboardingSeedPhraseState = uiState.copy( @@ -101,7 +130,7 @@ class StateBuilder( fun mnemonicGenerated( uiState: OnboardingSeedPhraseState, - mnemonicGridItems: ImmutableList, + mnemonicGridItems: PersistentList, ): OnboardingSeedPhraseState { return updateMnemonicComponents(uiState, mnemonicGridItems) .copy( @@ -116,9 +145,45 @@ class StateBuilder( ) } - fun updateMnemonicComponents( + fun selectSeedType( uiState: OnboardingSeedPhraseState, - mnemonicGridItems: ImmutableList, + mnemonicGridItems: PersistentList, + selectedSeedType: SegmentSeedType, + ): OnboardingSeedPhraseState = uiState.copy( + yourSeedPhraseState = uiState.yourSeedPhraseState.copy( + mnemonicGridItems = mnemonicGridItems, + segmentSeedState = uiState.yourSeedPhraseState.segmentSeedState.copy( + selectedSeedType = selectedSeedType, + ), + ), + ) + + fun clearCheckSeedPhraseState(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState = uiState.copy( + checkSeedPhraseState = uiState.checkSeedPhraseState.copy( + tvSecondPhrase = TextFieldState( + label = "2", + isFocused = true, + onTextFieldValueChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onTextFieldChanged, + onFocusChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onFocusChanged, + ), + tvSeventhPhrase = TextFieldState( + label = "7", + isFocused = false, + onTextFieldValueChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onTextFieldChanged, + onFocusChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onFocusChanged, + ), + tvEleventhPhrase = TextFieldState( + label = "11", + isFocused = false, + onTextFieldValueChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onTextFieldChanged, + onFocusChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onFocusChanged, + ), + ), + ) + + private fun updateMnemonicComponents( + uiState: OnboardingSeedPhraseState, + mnemonicGridItems: PersistentList, ): OnboardingSeedPhraseState = uiState.copy( yourSeedPhraseState = uiState.yourSeedPhraseState.copy( mnemonicGridItems = mnemonicGridItems, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt index 719abb7761..ed20db4319 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt @@ -13,18 +13,19 @@ import com.tangem.data.common.locale.LocaleProvider import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.domain.SeedPhraseError import com.tangem.feature.onboarding.domain.SeedPhraseInteractor +import com.tangem.feature.onboarding.domain.models.MnemonicType import com.tangem.feature.onboarding.presentation.wallet2.analytics.CreateWalletEvents import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseEvents import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.feature.onboarding.presentation.wallet2.model.* +import com.tangem.feature.onboarding.presentation.wallet2.model.SegmentSeedType.Companion.fromMnemonicType import com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders.StateBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer -import com.tangem.utils.extensions.isEven import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import javax.inject.Inject @@ -43,7 +44,8 @@ class SeedPhraseViewModel @Inject constructor( private var uiBuilder = StateBuilder(uiActions = createUiActions()) - var uiState: OnboardingSeedPhraseState by mutableStateOf(uiBuilder.init()) + private var selectedMnemonicType = MnemonicType.Mnemonic12 + var uiState: OnboardingSeedPhraseState by mutableStateOf(uiBuilder.init(fromMnemonicType(selectedMnemonicType))) private set private lateinit var router: SeedPhraseRouter @@ -70,7 +72,7 @@ class SeedPhraseViewModel @Inject constructor( private val textFieldsDebouncers = mutableMapOf() - private var generatedMnemonicComponents: List? = null + private var generatedMnemonicComponents: Map> = emptyMap() private var importedMnemonicComponents: List? = null override fun onCleared() { @@ -135,10 +137,13 @@ class SeedPhraseViewModel @Inject constructor( ), yourSeedPhraseActions = YourSeedPhraseUiAction( buttonContinueClick = ::buttonContinueClick, + onSelectType = ::onSelectType, ), checkSeedPhraseActions = CheckSeedPhraseUiAction( buttonCreateWalletClick = { - buttonImportWalletClick(generatedMnemonicComponents, SeedPhraseSource.GENERATED) + val mnemonicComponents = + generatedMnemonicComponents[selectedMnemonicType] ?: return@CheckSeedPhraseUiAction + buttonImportWalletClick(mnemonicComponents, SeedPhraseSource.GENERATED) }, secondTextFieldAction = TextFieldUiAction( onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Second, value) }, @@ -179,11 +184,12 @@ class SeedPhraseViewModel @Inject constructor( } createOrGetDebouncer(field.name).debounce(viewModelScope, context = dispatchers.io) { - val hasError = !interactor.isWordMatch(generatedMnemonicComponents, field, textFieldValue.text) + val mnemonicComponents = generatedMnemonicComponents[selectedMnemonicType] ?: return@debounce + val hasError = !interactor.isWordMatch(mnemonicComponents, field, textFieldValue.text) if (fieldState.isError != hasError) { updateUi { uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError) } } - val isCreateWalletButtonEnabled = SeedPhraseField.values() + val isCreateWalletButtonEnabled = SeedPhraseField.entries .map { field -> field.getState(uiState) } .all { fieldState -> fieldState.textFieldValue.text.isNotEmpty() && !fieldState.isError } @@ -324,50 +330,35 @@ class SeedPhraseViewModel @Inject constructor( } private fun buttonGenerateSeedPhraseClick() { + val mnemonicTypes = listOf(MnemonicType.Mnemonic12, MnemonicType.Mnemonic24) analyticsEventHandler.send(SeedPhraseEvents.ButtonGenerateSeedPhrase) launchSingle { updateUi { uiBuilder.generateMnemonicComponents(uiState) } delay(DELAY_GENERATE_SEED_PHRASE) - interactor.generateMnemonic() - .onSuccess { mnemonic -> - generatedMnemonicComponents = mnemonic.mnemonicComponents - val mnemonicGridItems = generateMnemonicGridList(mnemonic.mnemonicComponents) + interactor.generateMnemonics(mnemonicTypes) + .onSuccess { mnemonics -> + generatedMnemonicComponents = mnemonics.mapValues { it.value.mnemonicComponents } + val defaultMnemonic = mnemonics[selectedMnemonicType]?.mnemonicComponents ?: return@launchSingle + val mnemonicGridItems = toMnemonicGridList(defaultMnemonic) updateUi { router.openScreen(SeedPhraseScreen.YourSeedPhrase) - uiBuilder.mnemonicGenerated(uiState, mnemonicGridItems.toImmutableList()) + uiBuilder.mnemonicGenerated(uiState, mnemonicGridItems.toPersistentList()) } } .onFailure { - generatedMnemonicComponents = null + generatedMnemonicComponents = emptyMap() // TODO: show error } } } - private fun generateMnemonicGridList(mnemonicComponents: List): ImmutableList { - val size = mnemonicComponents.size - val splitIndex = if (size.isEven()) size / 2 else size / 2 + 1 - val leftColumn = mnemonicComponents.subList(0, splitIndex) - val rightColumn = mnemonicComponents.subList(splitIndex, size) - - val mnemonicGridItems = mutableListOf() - for (index in 0 until splitIndex) { - if (index <= leftColumn.size) { - val item = MnemonicGridItem( - index = index + 1, - mnemonic = leftColumn[index], - ) - mnemonicGridItems.add(item) - } - if (index < rightColumn.size) { - val item = MnemonicGridItem( - index = index + splitIndex + 1, - mnemonic = rightColumn[index], - ) - mnemonicGridItems.add(item) - } - } - return mnemonicGridItems.toImmutableList() + private fun toMnemonicGridList(mnemonicComponents: List): PersistentList { + return mnemonicComponents.mapIndexed { index, word -> + MnemonicGridItem( + index = index.inc(), + mnemonic = word, + ) + }.toPersistentList() } private fun buttonImportSeedPhraseClick() { @@ -376,7 +367,26 @@ class SeedPhraseViewModel @Inject constructor( } private fun buttonContinueClick() { - router.openScreen(SeedPhraseScreen.CheckSeedPhrase) + launchSingle { + updateUi { + uiBuilder.clearCheckSeedPhraseState(uiState) + } + router.openScreen(SeedPhraseScreen.CheckSeedPhrase) + } + } + + private fun onSelectType(selectedSeedType: SegmentSeedType) { + launchSingle { + selectedMnemonicType = selectedSeedType.toMnemonicType() + val selectedMnemonic = generatedMnemonicComponents[selectedMnemonicType] ?: return@launchSingle + updateUi { + uiBuilder.selectSeedType( + uiState = uiState, + mnemonicGridItems = toMnemonicGridList(selectedMnemonic), + selectedSeedType = selectedSeedType, + ) + } + } } private fun buttonSuggestedPhraseClick(suggestionIndex: Int) { @@ -433,9 +443,9 @@ class SeedPhraseViewModel @Inject constructor( } // endregion Utils - companion object { - private const val MNEMONIC_DEBOUNCER = "MnemonicDebouncer" - private const val MNEMONIC_DEBOUNCE_DELAY = 700L - private const val DELAY_GENERATE_SEED_PHRASE = 300L + private companion object { + const val MNEMONIC_DEBOUNCER = "MnemonicDebouncer" + const val MNEMONIC_DEBOUNCE_DELAY = 700L + const val DELAY_GENERATE_SEED_PHRASE = 300L } } \ No newline at end of file From 92a194c82834f1180fc647f1224ee8fa25a0454e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Mar 2024 21:02:23 +0100 Subject: [PATCH 048/109] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 6 +++--- core/res/src/main/res/values/strings.xml | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 41a397e283..b6ec8f4866 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -183,9 +183,9 @@ Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами Выберите провайдера Произошла ошибка. Код: %s - К сожалению обмен указанной пары через выбранного провайдера на данный момент не возможен. Попробуйте совершить обмен позже. (Код:\u00A0%s) - Выбранный провайдер не доступен для обмена. Попробуйте позже. (Код:\u00A0%s) - В данный момент обмен не возможен. Попробуйте позже. (Код:\u00A0%s) + К сожалению обмен указанной пары через выбранного провайдера на данный момент не возможен. Попробуйте совершить обмен позже. (Код: %s) + Выбранный провайдер не доступен для обмена. Попробуйте позже. (Код: %s) + В данный момент обмен не возможен. Попробуйте позже. (Код: %s) Курс обмена Обмен через %s Чтобы вернуть ваши деньги, посетите сайт провайдера diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 11cf3d5316..ce3ef5fed6 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -182,9 +182,9 @@ Tangem offers token swaps via 3rd-party providers according to each provider\'s terms Choose provider An error occurred. Code: %s - Oops! Swapping the selected pair through the chosen provider is temporarily unavailable. Please try again later. (Code:\u00A0%s) - Selected provider is unavailable at the moment. Please try again later. (Code:\u00A0%s) - Swaps are unavailable at the moment. Please try again later. (Code:\u00A0%s) + Oops! Swapping the selected pair through the chosen provider is temporarily unavailable. Please try again later. (Code: %s) + Selected provider is unavailable at the moment. Please try again later. (Code: %s) + Swaps are unavailable at the moment. Please try again later. (Code: %s) Estimated amount Exchange by %s Visit provider’s website to refund your money @@ -482,6 +482,7 @@ Send to A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds My wallets + Sending... Tap any field to change it Send %s You are sending %1$s including a network fee of %2$s From ee85f09aa0a20970a1e32974a13bd3aeb5669155 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Mar 2024 18:28:36 +0500 Subject: [PATCH 049/109] Updated on 2026-08-14 --- .../presentation/state/SendStateFactory.kt | 7 +- .../impl/presentation/state/StateRouter.kt | 10 +- .../state/fee/FeeNotificationFactory.kt | 1 - .../state/fee/FeeSelectorState.kt | 4 +- .../presentation/state/fee/FeeStateFactory.kt | 4 +- .../state/fee/SendFeeStateConverter.kt | 2 +- .../state/previewdata/FeeStatePreviewData.kt | 4 + .../state/previewdata/SendClickIntentsStub.kt | 63 ++++ .../presentation/ui/SendNavigationButtons.kt | 18 +- .../ui/fee/SendSpeedAndFeeContent.kt | 52 +-- .../presentation/ui/fee/SendSpeedSelector.kt | 325 +++--------------- .../ui/fee/SendSpeedSelectorItem.kt | 173 ++++++++++ .../presentation/viewmodel/SendViewModel.kt | 77 +++-- 13 files changed, 389 insertions(+), 351 deletions(-) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index b5f1d0f72e..f50a061117 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -243,7 +243,12 @@ internal class SendStateFactory( fun getSendingStateUpdate(isSending: Boolean): SendUiState { val state = currentStateProvider() - return state.copy(sendState = state.sendState?.copy(isSending = isSending)) + return state.copy( + sendState = state.sendState?.copy( + isSending = isSending, + isPrimaryButtonEnabled = !isSending, + ), + ) } fun getTransactionSendState(txData: TransactionData): SendUiState { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index de09a0570e..079dbfdb6f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -36,8 +36,8 @@ internal class StateRouter( } else -> when (type) { SendUiStateType.Amount -> continueToSend(::showRecipient) - SendUiStateType.Fee -> continueToSend(::showAmount) - SendUiStateType.Send -> continueToSend(::showFee) + SendUiStateType.Fee -> showSend() + SendUiStateType.Send -> continueToSend(::showAmount) else -> continueToSend(::popBackStack) } } @@ -46,8 +46,9 @@ internal class StateRouter( fun onNextClick() { when (currentState.value.type) { SendUiStateType.Recipient -> continueToSend(::showAmount) - SendUiStateType.Amount -> continueToSend(::showFee) - SendUiStateType.Fee -> showSend() + SendUiStateType.Amount, + SendUiStateType.Fee, + -> showSend() SendUiStateType.Send -> onBackClick() else -> popBackStack() } @@ -59,7 +60,6 @@ internal class StateRouter( } else { when (currentState.value.type) { SendUiStateType.Amount -> showRecipient() - SendUiStateType.Fee -> showAmount() else -> popBackStack() } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt index aee69c85fb..217631c38e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt @@ -30,7 +30,6 @@ internal class FeeNotificationFactory( val feeState = state.feeState ?: return@map persistentListOf() buildList { when (val feeSelectorState = feeState.feeSelectorState) { - FeeSelectorState.Loading -> Unit FeeSelectorState.Error -> { addFeeUnreachableNotification(feeSelectorState) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt index e0cda82bfa..67fe86cf8e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt @@ -9,15 +9,13 @@ import kotlinx.collections.immutable.persistentListOf @Immutable internal sealed class FeeSelectorState { - object Loading : FeeSelectorState() - data class Content( val fees: TransactionFee, val selectedFee: FeeType = FeeType.Market, val customValues: ImmutableList = persistentListOf(), ) : FeeSelectorState() - object Error : FeeSelectorState() + data object Error : FeeSelectorState() } enum class FeeType { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index d2339a3de9..997c39d2f1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -45,8 +45,8 @@ internal class FeeStateFactory( val state = currentStateProvider() val feeState = state.feeState ?: return state return state.copy( + amountState = state.amountState?.copy(isFeeLoading = true), feeState = feeState.copy( - feeSelectorState = FeeSelectorState.Loading, notifications = persistentListOf(), isPrimaryButtonEnabled = false, ), @@ -65,6 +65,7 @@ internal class FeeStateFactory( val fee = feeConverter.convert(feeSelectorState) return state.copy( + amountState = state.amountState?.copy(isFeeLoading = false), feeState = feeState.copy( feeSelectorState = feeSelectorState, fee = fee, @@ -76,6 +77,7 @@ internal class FeeStateFactory( fun onFeeOnErrorState(): SendUiState { val state = currentStateProvider() return state.copy( + amountState = state.amountState?.copy(isFeeLoading = false), feeState = state.feeState?.copy( feeSelectorState = FeeSelectorState.Error, ), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt index 864ae1b501..caf0ee5683 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -14,7 +14,7 @@ internal class SendFeeStateConverter( override fun convert(value: Unit): SendStates.FeeState { return SendStates.FeeState( - feeSelectorState = FeeSelectorState.Loading, + feeSelectorState = FeeSelectorState.Error, fee = null, notifications = persistentListOf(), rate = feeCryptoCurrencyStatusProvider().value.fiatRate, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt index 7cec508963..c07c45a39b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt @@ -49,4 +49,8 @@ internal object FeeStatePreviewData { isFeeApproximate = false, notifications = persistentListOf(), ) + + val errorFeeState = feeState.copy( + feeSelectorState = FeeSelectorState.Error, + ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt new file mode 100644 index 0000000000..d70b281ffe --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt @@ -0,0 +1,63 @@ +package com.tangem.features.send.impl.presentation.state.previewdata + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource +import com.tangem.features.send.impl.presentation.state.SendNotification +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents + +@Suppress("TooManyFunctions") +internal object SendClickIntentsStub : SendClickIntents { + override fun popBackStack() {} + + override fun onBackClick() {} + + override fun onNextClick() {} + + override fun onPrevClick() {} + + override fun onQrCodeScanClick() {} + + override fun onFailedTxEmailClick(errorMessage: String) {} + + override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) {} + + override fun onAmountValueChange(value: String) {} + + override fun onCurrencyChangeClick(isFiat: Boolean) {} + + override fun onMaxValueClick() {} + + override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {} + + override fun onRecipientMemoValueChange(value: String) {} + + override fun feeReload() {} + + override fun onFeeSelectorClick(feeType: FeeType) {} + + override fun onCustomFeeValueChange(index: Int, value: String) {} + + override fun onSubtractSelect() {} + + override fun onReadMoreClick() {} + + override fun onSendClick() {} + + override fun showAmount() {} + + override fun showRecipient() {} + + override fun showFee() {} + + override fun showSend() {} + + override fun onExploreClick() {} + + override fun onShareClick() {} + + override fun onAmountReduceClick(reducedAmount: String, clazz: Class) {} + + override fun onNotificationCancel(clazz: Class) {} +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 59028beb85..f7252345fd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -69,14 +69,17 @@ private fun SendNavigationButton( val sendState = uiState.sendState ?: return val isEditingDisabled = uiState.isEditingDisabled val isSuccess = sendState.isSuccess + val isSending = sendState.isSending val isFromConfirmation = currentState.isFromConfirmation val isCorrectScreen = currentState.type == SendUiStateType.Amount || currentState.type == SendUiStateType.Fee - val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess + val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess && !isSending + val showProgress = uiState.amountState?.isFeeLoading == true val (buttonTextId, buttonClick) = getButtonData( currentState = currentState, isSuccess = isSuccess, + isSending = isSending, uiState = uiState, ) val isButtonEnabled = isButtonEnabled(currentState, uiState) @@ -101,7 +104,7 @@ private fun SendNavigationButton( modifier = Modifier .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) .background(TangemTheme.colors.button.secondary) - .clickable { uiState.clickIntents.onPrevClick() } + .clickable(enabled = !showProgress) { uiState.clickIntents.onPrevClick() } .padding(TangemTheme.dimens.spacing12), ) } @@ -113,7 +116,7 @@ private fun SendNavigationButton( if (isSendingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) buttonClick() }, - showProgress = sendState.isSending, + showProgress = showProgress, modifier = Modifier.fillMaxWidth(), colors = TangemButtonsDefaults.primaryButtonColors, ) @@ -204,6 +207,7 @@ private fun getButtonData( uiState: SendUiState, currentState: SendUiCurrentScreen, isSuccess: Boolean, + isSending: Boolean, ): Pair Unit> { return when (currentState.type) { SendUiStateType.None, @@ -215,10 +219,10 @@ private fun getButtonData( } else { R.string.common_next to uiState.clickIntents::onNextClick } - SendUiStateType.Send -> if (isSuccess) { - R.string.common_close - } else { - R.string.common_send + SendUiStateType.Send -> when { + isSuccess -> R.string.common_close + isSending -> R.string.send_sending + else -> R.string.common_send } to uiState.clickIntents::onSendClick } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt index f9e410b2ee..c17e9c4458 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -1,6 +1,5 @@ package com.tangem.features.send.impl.presentation.ui.fee -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize @@ -55,17 +54,13 @@ private fun LazyListScope.feeSelector(state: SendStates.FeeState, clickIntents: } @OptIn(ExperimentalFoundationApi::class) -private fun LazyListScope.notifications( - configs: ImmutableList, - modifier: Modifier = Modifier, - isLast: Boolean = false, -) { +private fun LazyListScope.notifications(configs: ImmutableList, modifier: Modifier = Modifier) { items( items = configs, key = { it::class.java }, contentType = { it::class.java }, itemContent = { - val bottomPadding = if (isLast) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing0 + val bottomPadding = if (it == configs.last()) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing0 Notification( config = it.config, modifier = modifier @@ -75,21 +70,9 @@ private fun LazyListScope.notifications( ) .animateItemPlacement(), containerColor = when (it) { - is SendNotification.Error.ExceedsBalance, - is SendNotification.Warning.NetworkFeeUnreachable, - -> TangemTheme.colors.background.action + is SendNotification.Warning.NetworkFeeUnreachable -> TangemTheme.colors.background.action else -> TangemTheme.colors.button.disabled }, - iconTint = when (it) { - is SendNotification.Error.ExceedsBalance -> { - if (it.config.buttonsState == null) { - TangemTheme.colors.icon.warning - } else { - null - } - } - else -> null - }, ) }, ) @@ -97,24 +80,19 @@ private fun LazyListScope.notifications( @OptIn(ExperimentalFoundationApi::class) internal fun LazyListScope.customFee(feeSendState: FeeSelectorState, modifier: Modifier = Modifier) { - item( - key = FEE_CUSTOM_KEY, - ) { - AnimatedVisibility( - visible = feeSendState is FeeSelectorState.Content, - modifier = modifier - .fillMaxWidth() - .animateItemPlacement() - .background(TangemTheme.colors.background.tertiary), + if (feeSendState is FeeSelectorState.Content) { + item( + key = FEE_CUSTOM_KEY, ) { - (feeSendState as? FeeSelectorState.Content)?.let { fee -> - val customValues = fee.customValues - SendCustomFeeEthereum( - customValues = customValues, - selectedFee = fee.selectedFee, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), - ) - } + SendCustomFeeEthereum( + customValues = feeSendState.customValues, + selectedFee = feeSendState.selectedFee, + modifier = modifier + .fillMaxWidth() + .animateItemPlacement() + .background(TangemTheme.colors.background.tertiary) + .padding(top = TangemTheme.dimens.spacing12), + ) } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index dfaed7cb57..270d4954f9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -1,56 +1,29 @@ package com.tangem.features.send.impl.presentation.ui.fee -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.ClickableText -import androidx.compose.material3.Icon -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.components.rows.SelectorRowItem -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.stringReference +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.core.ui.utils.BigDecimalFormatter.CAN_BE_LOWER_SIGN -import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendStates -import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.state.previewdata.FeeStatePreviewData +import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents -import java.math.BigDecimal - -private val DEFAULT_FEE_OPTIONS = listOf( - R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24, - R.string.common_fee_selector_option_market to R.drawable.ic_bird_24, - R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24, -) @Suppress("LongMethod") @Composable @@ -66,79 +39,34 @@ internal fun SendSpeedSelector( .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action), ) { - when (val feeSelectorState = state.feeSelectorState) { - FeeSelectorState.Error -> { - SendSpeedSelectorItemError() - } - FeeSelectorState.Loading -> { - SendSpeedSelectorItemLoading() - } - is FeeSelectorState.Content -> { - when (val fees = feeSelectorState.fees) { - is TransactionFee.Choosable -> { - val isSelected = feeSelectorState.selectedFee - val minimumAmount = fees.minimum.amount - SendSpeedSelectorItem( - titleRes = R.string.common_fee_selector_option_slow, - iconRes = R.drawable.ic_tortoise_24, - amount = getCryptoReference(minimumAmount, state.isFeeApproximate), - fiatAmount = getFiatReference(minimumAmount, state.rate, state.appCurrency), - symbolLength = minimumAmount.currencySymbol.length, - isSelected = isSelected == FeeType.Slow, - onSelect = { clickIntents.onFeeSelectorClick(FeeType.Slow) }, - ) - val normalAmount = fees.normal.amount - SendSpeedSelectorItem( - titleRes = R.string.common_fee_selector_option_market, - iconRes = R.drawable.ic_bird_24, - amount = getCryptoReference(normalAmount, state.isFeeApproximate), - fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency), - symbolLength = normalAmount.currencySymbol.length, - isSelected = isSelected == FeeType.Market, - onSelect = { clickIntents.onFeeSelectorClick(FeeType.Market) }, - ) - val priorityAmount = fees.priority.amount - SendSpeedSelectorItem( - titleRes = R.string.common_fee_selector_option_fast, - iconRes = R.drawable.ic_hare_24, - amount = getCryptoReference(priorityAmount, state.isFeeApproximate), - fiatAmount = getFiatReference(priorityAmount, state.rate, state.appCurrency), - symbolLength = priorityAmount.currencySymbol.length, - isSelected = isSelected == FeeType.Fast, - onSelect = { clickIntents.onFeeSelectorClick(FeeType.Fast) }, - showDivider = fees.normal is Fee.Ethereum, - ) - AnimatedVisibility( - visible = fees.normal is Fee.Ethereum, - label = "Custom fee appearance animation", - ) { - val showWarning = state.notifications.any { it is SendNotification.Warning.TooHigh } - SendSpeedSelectorItem( - titleRes = R.string.common_fee_selector_option_custom, - iconRes = R.drawable.ic_edit_24, - isSelected = isSelected == FeeType.Custom, - onSelect = { clickIntents.onFeeSelectorClick(FeeType.Custom) }, - showDivider = fees.normal !is Fee.Ethereum, - showWarning = showWarning, - ) - } - } - is TransactionFee.Single -> { - val normalAmount = feeSelectorState.fees.normal.amount - SendSpeedSelectorItem( - titleRes = R.string.common_fee_selector_option_market, - iconRes = R.drawable.ic_bird_24, - isSelected = true, - amount = getCryptoReference(normalAmount, state.isFeeApproximate), - fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency), - symbolLength = normalAmount.currencySymbol.length, - onSelect = { clickIntents.onFeeSelectorClick(FeeType.Market) }, - showDivider = false, - ) - } - } - } - } + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_slow, + iconRes = R.drawable.ic_tortoise_24, + feeType = FeeType.Slow, + state = state, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.Slow) }, + ) + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_market, + iconRes = R.drawable.ic_bird_24, + feeType = FeeType.Market, + state = state, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.Market) }, + ) + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_fast, + iconRes = R.drawable.ic_hare_24, + feeType = FeeType.Fast, + state = state, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.Fast) }, + ) + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_custom, + iconRes = R.drawable.ic_edit_24, + feeType = FeeType.Custom, + state = state, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.Custom) }, + ) } FooterText(clickIntents::onReadMoreClick) } @@ -177,179 +105,32 @@ private fun FooterText(onReadMoreClick: () -> Unit) { ) } -// todo remove after refactoring [REDACTED_JIRA] -private fun getCryptoReference(amount: Amount, isFeeApproximate: Boolean) = combinedReference( - if (isFeeApproximate) stringReference("$CAN_BE_LOWER_SIGN ") else TextReference.EMPTY, - stringReference( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = amount.value, - cryptoCurrency = amount.currencySymbol, - decimals = amount.decimals, - ), - ), -) - -// todo remove after refactoring [REDACTED_JIRA] -private fun getFiatReference(amount: Amount, rate: BigDecimal?, appCurrency: AppCurrency) = stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = rate?.let { amount.value?.multiply(it) }, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), -) - -@Composable -private fun SendSpeedSelectorItemLoading() { - repeat(DEFAULT_FEE_OPTIONS.size) { - val (text, iconRes) = DEFAULT_FEE_OPTIONS[it] - Row(modifier = Modifier.fillMaxWidth()) { - SelectorTitleContent( - titleRes = text, - iconRes = iconRes, - ) - SpacerWMax() - RectangleShimmer( - radius = TangemTheme.dimens.radius3, - modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing18, - bottom = TangemTheme.dimens.spacing18, - end = TangemTheme.dimens.spacing12, - ) - .size( - width = TangemTheme.dimens.size90, - height = TangemTheme.dimens.size12, - ), - ) - } - } -} - -@Composable -private fun SendSpeedSelectorItemError() { - repeat(DEFAULT_FEE_OPTIONS.size) { - val (text, iconRes) = DEFAULT_FEE_OPTIONS[it] - Row(modifier = Modifier.fillMaxWidth()) { - SelectorTitleContent( - titleRes = text, - iconRes = iconRes, - ) - SpacerWMax() - Text( - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding( - vertical = TangemTheme.dimens.spacing14, - horizontal = TangemTheme.dimens.spacing12, - ), - ) - } - } -} - -@Composable -private fun SendSpeedSelectorItem( - @StringRes titleRes: Int, - @DrawableRes iconRes: Int, - onSelect: () -> Unit, - modifier: Modifier = Modifier, - amount: TextReference? = null, - fiatAmount: TextReference? = null, - symbolLength: Int? = null, - isSelected: Boolean = false, - showDivider: Boolean = true, - showWarning: Boolean = false, -) { - Box( - modifier = modifier - .fillMaxWidth() - .clickable { onSelect() }, - ) { - SelectorRowItem( - titleRes = titleRes, - iconRes = iconRes, - onSelect = onSelect, - modifier = modifier, - preDot = amount, - postDot = fiatAmount, - ellipsizeOffset = symbolLength, - isSelected = isSelected, - showDivider = showDivider, - ) - WarningIcon(showWarning = showWarning) - } -} - -@Composable -private fun SelectorTitleContent( - @StringRes titleRes: Int, - @DrawableRes iconRes: Int, - iconTint: Color = TangemTheme.colors.icon.informative, - textStyle: TextStyle = TangemTheme.typography.body2, -) { - Icon( - painter = painterResource(iconRes), - tint = iconTint, - contentDescription = null, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ), - ) - Text( - text = stringResource(titleRes), - style = textStyle, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing8, - top = TangemTheme.dimens.spacing14, - bottom = TangemTheme.dimens.spacing14, - ), - ) -} - -@Composable -private fun WarningIcon(showWarning: Boolean = false) { - AnimatedVisibility( - visible = showWarning, - label = "Custom fee warning indicator", - enter = fadeIn(), - exit = fadeOut(), - ) { - Row { - SpacerWMax() - Image( - painter = painterResource(R.drawable.ic_alert_triangle_20), - contentDescription = null, - modifier = Modifier - .padding( - vertical = TangemTheme.dimens.spacing12, - horizontal = TangemTheme.dimens.spacing14, - ), - ) - } - } -} - -//region preview +// region Preview @Preview @Composable -private fun FeeSelectorPreview_Light() { +private fun SendSpeedSelectorPreview_Light( + @PreviewParameter(SendSpeedSelectorPreviewProvider::class) feeState: SendStates.FeeState, +) { TangemTheme { - SendSpeedSelectorItemLoading() + SendSpeedSelector(state = feeState, clickIntents = SendClickIntentsStub) } } @Preview @Composable -private fun FeeSelectorPreview_Dark() { +private fun SendSpeedSelectorPreview_Dark( + @PreviewParameter(SendSpeedSelectorPreviewProvider::class) feeState: SendStates.FeeState, +) { TangemTheme(isDark = true) { - SendSpeedSelectorItemLoading() + SendSpeedSelector(state = feeState, clickIntents = SendClickIntentsStub) } } -//endregion \ No newline at end of file + +private class SendSpeedSelectorPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + FeeStatePreviewData.feeState, + FeeStatePreviewData.errorFeeState, + ) +} +// endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt new file mode 100644 index 0000000000..2caaa43e1b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -0,0 +1,173 @@ +package com.tangem.features.send.impl.presentation.ui.fee + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.animation.* +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendNotification +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import java.math.BigDecimal + +@Composable +internal fun SendSpeedSelectorItem( + @StringRes titleRes: Int, + @DrawableRes iconRes: Int, + feeType: FeeType, + state: SendStates.FeeState, + onSelect: () -> Unit, + modifier: Modifier = Modifier, +) { + val feeSelectorState = state.feeSelectorState + val content = feeSelectorState as? FeeSelectorState.Content + val amount = content?.getAmount(feeType) + val (showDivider, isVisible) = content.getDividerAndVisibility(feeType) + val hasCustomValues = !content?.customValues.isNullOrEmpty() + AnimatedVisibility( + visible = isVisible || hasCustomValues, + label = "Fee Selector Visibility Animation", + enter = expandVertically().plus(fadeIn()), + exit = shrinkVertically().plus(fadeOut()), + ) { + Box( + modifier = modifier + .fillMaxWidth() + .clickable { onSelect() }, + ) { + SelectorRowItem( + titleRes = titleRes, + iconRes = iconRes, + onSelect = onSelect, + modifier = modifier, + preDot = getCryptoReference(amount, state.isFeeApproximate), + postDot = getFiatReference(amount, state.rate, state.appCurrency), + ellipsizeOffset = amount?.currencySymbol?.length, + isSelected = content?.selectedFee == feeType, + showDivider = showDivider, + ) + SendSpeedSelectorItemError(isError = feeSelectorState is FeeSelectorState.Error) + + if (feeType == FeeType.Custom) { + val showWarning = state.notifications.any { it is SendNotification.Warning.TooHigh } + WarningIcon(showWarning = showWarning) + } + } + } +} + +// todo remove after refactoring [REDACTED_JIRA] +private fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { + if (amount == null) return null + return combinedReference( + if (isFeeApproximate) stringReference("${BigDecimalFormatter.CAN_BE_LOWER_SIGN} ") else TextReference.EMPTY, + stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = amount.value, + cryptoCurrency = amount.currencySymbol, + decimals = amount.decimals, + ), + ), + ) +} + +// todo remove after refactoring [REDACTED_JIRA] +private fun getFiatReference(amount: Amount?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { + if (amount == null) return null + return stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = rate?.let { amount.value?.multiply(it) }, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ) +} + +@Composable +private fun SendSpeedSelectorItemError(isError: Boolean) { + Row { + SpacerWMax() + AnimatedVisibility( + visible = isError, + label = "Error state indication animation", + enter = fadeIn(), + exit = fadeOut(), + ) { + Text( + text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + vertical = TangemTheme.dimens.spacing14, + horizontal = TangemTheme.dimens.spacing12, + ), + ) + } + } +} + +@Composable +private fun WarningIcon(showWarning: Boolean = false) { + Row { + SpacerWMax() + AnimatedVisibility( + visible = showWarning, + label = "Custom fee warning indicator", + enter = fadeIn(), + exit = fadeOut(), + ) { + Image( + painter = painterResource(R.drawable.ic_alert_triangle_20), + contentDescription = null, + modifier = Modifier + .padding( + vertical = TangemTheme.dimens.spacing12, + horizontal = TangemTheme.dimens.spacing14, + ), + ) + } + } +} + +private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? { + val choosableFees = fees as? TransactionFee.Choosable + return when (feeType) { + FeeType.Slow -> choosableFees?.minimum?.amount + FeeType.Market -> fees.normal.amount + FeeType.Fast -> choosableFees?.priority?.amount + FeeType.Custom -> null + } +} + +private fun FeeSelectorState.Content?.getDividerAndVisibility(feeType: FeeType): Pair { + val hasCustomValues = !this?.customValues.isNullOrEmpty() + val isNotSingle = this?.fees !is TransactionFee.Single + val isSingle = this?.fees is TransactionFee.Single + return when (feeType) { + FeeType.Slow -> true to isNotSingle + FeeType.Market -> isNotSingle to isSingle + FeeType.Fast -> hasCustomValues to isNotSingle + FeeType.Custom -> false to isNotSingle + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index fd799169cc..decc2cc4d6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -485,26 +485,22 @@ internal class SendViewModel @Inject constructor( override fun onBackClick() = stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true) override fun onNextClick() { val currentState = stateRouter.currentState.value - val isCurrentFee = currentState.type == SendUiStateType.Fee - if (isCurrentFee) { - val isFeeCoverage = checkFeeCoverage(uiState, cryptoCurrencyStatus) - if (isAmountSubtractAvailable && isFeeCoverage) { - uiState = eventStateFactory.getFeeCoverageAlert( - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, - ) - return - } else { - analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false)) + sendOnNextScreenAnalyticSender.send(currentState.type, uiState) + when (currentState.type) { + SendUiStateType.Fee -> { + if (onFeeNext()) return } - if (checkIfFeeTooLow(uiState)) { - uiState = eventStateFactory.getFeeTooLowAlert( - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, - ) - return + SendUiStateType.Amount -> { + if (uiState.feeState?.feeSelectorState is FeeSelectorState.Content) { + if (onFeeCoverageAlert()) return + } else { + loadFee(isToNextState = true) + return + } } + else -> Unit } - sendOnNextScreenAnalyticSender.send(currentState.type, uiState) stateRouter.onNextClick() } @@ -521,6 +517,30 @@ internal class SendViewModel @Inject constructor( override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) = innerRouter.openTokenDetails(userWalletId, currency) + + private fun onFeeNext(): Boolean { + if (onFeeCoverageAlert()) return true + if (checkIfFeeTooLow(uiState)) { + uiState = eventStateFactory.getFeeTooLowAlert( + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + return true + } + return false + } + + private fun onFeeCoverageAlert(): Boolean { + val isFeeCoverage = checkFeeCoverage(uiState, cryptoCurrencyStatus) + return if (isAmountSubtractAvailable && isFeeCoverage) { + uiState = eventStateFactory.getFeeCoverageAlert( + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + true + } else { + analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false)) + false + } + } // endregion // region amount state clicks @@ -648,15 +668,26 @@ internal class SendViewModel @Inject constructor( innerRouter.openUrl(url) } - private fun loadFee() { + private fun loadFee(isToNextState: Boolean = false) { viewModelScope.launch(dispatchers.main) { - if (uiState.feeState?.fee == null) { + val isShowStatus = uiState.feeState?.fee == null + if (isShowStatus) { uiState = feeStateFactory.onFeeOnLoadingState() } - uiState = callFeeUseCase()?.fold( - ifRight = feeStateFactory::onFeeOnLoadedState, - ifLeft = { feeStateFactory.onFeeOnErrorState() }, - ) ?: feeStateFactory.onFeeOnErrorState() + val result = callFeeUseCase()?.fold( + ifRight = { + uiState = feeStateFactory.onFeeOnLoadedState(it) + if (isToNextState && !onFeeCoverageAlert()) { + stateRouter.showSend() + } + }, + ifLeft = { + if (isShowStatus) uiState = feeStateFactory.onFeeOnErrorState() + }, + ) + if (result == null && isShowStatus) { + uiState = feeStateFactory.onFeeOnErrorState() + } updateFeeNotifications() updateAmountNotifications() }.saveIn(feeJobHolder) @@ -733,7 +764,7 @@ internal class SendViewModel @Inject constructor( override fun onAmountReduceClick(reducedAmount: String, clazz: Class) { uiState = amountStateFactory.getOnAmountValueChange(reducedAmount) uiState = sendNotificationFactory.dismissNotificationState(clazz) - loadFee() + onCheckFeeUpdate() } override fun onNotificationCancel(clazz: Class) { From 2754318b8ea33c2c2cb2e24ff9185991e0526eb2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Mar 2024 15:18:29 +0500 Subject: [PATCH 050/109] Updated on 2026-08-14 --- .../ui/components/fields/AmountTextField.kt | 10 +++++----- .../com/tangem/utils/coroutines/JobHolder.kt | 7 ++++--- .../state/amount/AmountStateFactory.kt | 7 +++++++ .../presentation/ui/SendNavigationButtons.kt | 2 +- .../presentation/ui/amount/AmountButtons.kt | 2 ++ .../presentation/ui/amount/AmountField.kt | 3 ++- .../ui/amount/AmountFieldContainer.kt | 1 + .../ui/amount/SendAmountContent.kt | 6 +++++- .../presentation/viewmodel/SendViewModel.kt | 20 +++++++++++++++++-- 9 files changed, 45 insertions(+), 13 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index fee185d0ab..fb140e00d4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -64,6 +64,7 @@ fun AmountTextField( keyboardType = KeyboardType.Number, ), keyboardActions: KeyboardActions = KeyboardActions.Default, + isEnabled: Boolean = true, isAutoResize: Boolean = false, @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) reduceFactor: Double = 0.9, @@ -106,21 +107,20 @@ fun AmountTextField( textStyle = textStyle.copy( fontSize = fontSize, textDirection = TextDirection.ContentOrLtr, - textAlign = TextAlign.Center, ), color = color, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, singleLine = true, + readOnly = !isEnabled, visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat), modifier = Modifier.background(TangemTheme.colors.background.action), decorationBox = { innerTextField -> Box { if (value.isBlank() && showPlaceholder) { - val placeholder = if (symbol != null) { - decimalFormat.defaultFormat().plus(" $symbol") - } else { - decimalFormat.defaultFormat() + var placeholder = decimalFormat.defaultFormat() + if (symbol != null) { + placeholder = placeholder.plus(" $symbol") } Text( text = placeholder, diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt index 42c4c7b296..14e64f1b62 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt @@ -11,10 +11,11 @@ class JobHolder { private var job: Job? = null - /** Update current [job] */ - fun update(job: Job?) { + /** Update current [JobHolder.job] and return new [job] */ + fun update(job: Job): Job { this.job?.cancel() this.job = job + return job } /** Cancel current [job] */ @@ -23,4 +24,4 @@ class JobHolder { } } -fun Job.saveIn(jobHolder: JobHolder) = jobHolder.update(job = this) \ No newline at end of file +fun Job.saveIn(jobHolder: JobHolder): Job = jobHolder.update(job = this) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt index 626da0c89b..4577927cc1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt @@ -50,4 +50,11 @@ internal class AmountStateFactory( amountState = state.amountState?.copy(notifications = notifications), ) } + + fun getOnAmountFeeLoadingCancel(): SendUiState { + val state = currentStateProvider() + return state.copy( + amountState = state.amountState?.copy(isFeeLoading = false), + ) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index f7252345fd..94b544a15e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -104,7 +104,7 @@ private fun SendNavigationButton( modifier = Modifier .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) .background(TangemTheme.colors.button.secondary) - .clickable(enabled = !showProgress) { uiState.clickIntents.onPrevClick() } + .clickable { uiState.clickIntents.onPrevClick() } .padding(TangemTheme.dimens.spacing12), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt index 68573bf3c7..00af1e90e7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt @@ -28,6 +28,7 @@ private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey" internal fun LazyListScope.buttons( segmentedButtonConfig: PersistentList, clickIntents: SendClickIntents, + isMaxButtonEnabled: Boolean, ) { item( key = AMOUNT_BUTTONS_KEY, @@ -55,6 +56,7 @@ internal fun LazyListScope.buttons( } SecondaryButton( text = stringResource(R.string.send_max_amount), + enabled = isMaxButtonEnabled, onClick = { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) clickIntents.onMaxValueClick() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt index d9ef26bd3c..e6206028fe 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -22,7 +22,7 @@ import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.send.impl.presentation.state.fields.SendTextField @Composable -internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean) { +internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean, isEnabled: Boolean) { val decimalFormat = rememberDecimalFormat() val (primaryValue, secondaryValue) = if (isFiat) { sendField.fiatValue to sendField.value @@ -47,6 +47,7 @@ internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean) color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, ), + isEnabled = isEnabled, placeholderAlignment = TopCenter, modifier = Modifier .padding( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt index 9b44cc2e1f..836ffd79a8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt @@ -63,6 +63,7 @@ internal fun LazyListScope.amountField( AmountField( sendField = amountState.amountTextField, isFiat = amountState.amountTextField.isFiatValue, + isEnabled = !amountState.isFeeLoading, ) } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt index 3f3d18d3bd..f38b4c48a1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt @@ -28,7 +28,11 @@ internal fun SendAmountContent( .background(TangemTheme.colors.background.tertiary), ) { amountField(amountState = amountState, isBalanceHiding = isBalanceHiding) - buttons(amountState.segmentedButtonConfig, clickIntents) + buttons( + segmentedButtonConfig = amountState.segmentedButtonConfig, + clickIntents = clickIntents, + isMaxButtonEnabled = !amountState.isFeeLoading, + ) notifications(amountState.notifications) } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index decc2cc4d6..67e152dae9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -482,7 +482,11 @@ internal class SendViewModel @Inject constructor( // region screen state navigation override fun popBackStack() = stateRouter.popBackStack() - override fun onBackClick() = stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true) + override fun onBackClick() { + cancelFeeRequest() + stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true) + } + override fun onNextClick() { val currentState = stateRouter.currentState.value sendOnNextScreenAnalyticSender.send(currentState.type, uiState) @@ -504,7 +508,10 @@ internal class SendViewModel @Inject constructor( stateRouter.onNextClick() } - override fun onPrevClick() = stateRouter.onPrevClick() + override fun onPrevClick() { + cancelFeeRequest() + stateRouter.onPrevClick() + } override fun onQrCodeScanClick() { analyticsEventHandler.send(SendAnalyticEvents.QrCodeButtonClicked) @@ -541,6 +548,12 @@ internal class SendViewModel @Inject constructor( false } } + + private fun cancelFeeRequest() { + viewModelScope.launch(dispatchers.main) { + feeJobHolder.cancel() + } + } // endregion // region amount state clicks @@ -691,6 +704,9 @@ internal class SendViewModel @Inject constructor( updateFeeNotifications() updateAmountNotifications() }.saveIn(feeJobHolder) + .invokeOnCompletion { + uiState = amountStateFactory.getOnAmountFeeLoadingCancel() + } } private suspend fun checkIfSubtractAvailable() { From 24cd5e1d400e2dab6aa9132d8d73bb901a5cb7d9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Mar 2024 16:46:48 +0500 Subject: [PATCH 051/109] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 2 +- .../AppBarWithBackButtonAndIconContent.kt | 1 + .../impl/presentation/state/SendAlertState.kt | 4 +- .../state/SendEventStateFactory.kt | 8 +++ .../presentation/state/SendNotification.kt | 10 +-- .../impl/presentation/state/SendUiState.kt | 2 +- .../state/confirm/SendNotificationFactory.kt | 2 +- .../ui/amount/SendAmountContent.kt | 32 +-------- .../presentation/ui/common/Notifications.kt | 58 ++++++++++++++++ .../ui/fee/SendCustomFeeEthereum.kt | 11 ++- .../ui/fee/SendSpeedAndFeeContent.kt | 68 ++++++------------- .../ui/recipient/SendRecipientContent.kt | 1 - .../impl/presentation/ui/send/SendContent.kt | 36 +--------- 14 files changed, 114 insertions(+), 123 deletions(-) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index b6ec8f4866..f71126d7f2 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -425,7 +425,7 @@ Приготовьте свою карту Уже содержится в введенном адресе Вычесть - Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму? + Недостаточно средств для покрытия комиссии сети. Вычесть комиссию %s из отправляемой сумму? Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? Причина: %1$s\nКод: %2$s Транзакция не выполнена diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index ce3ef5fed6..8e0afa7d1e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -421,7 +421,7 @@ Get your card ready! Already included in the entered address Subtract - Not enough funds to cover the network commission. Subtract the commission from the amount sent? + Not enough funds to cover the network commission. Subtract the commission %s from the amount sent? You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue? Reason: %1$s\nCode: %2$s The transaction is not completed diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt index 229fbe444c..2bd0141ea1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt @@ -45,6 +45,7 @@ fun AppBarWithBackButtonAndIconContent( ) { Row( modifier = modifier + .height(TangemTheme.dimens.size56) .background(color = backgroundColor) .fillMaxWidth() .padding(horizontal = TangemTheme.dimens.spacing16), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt index 30becd1f7e..93bb812fae 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt @@ -59,10 +59,12 @@ internal sealed class SendAlertState { } data class FeeCoverage( + val amount: String, override val onConfirmClick: (() -> Unit), ) : SendAlertState() { override val title: TextReference? = null - override val message: TextReference = resourceReference(id = R.string.send_alert_fee_coverage_title) + override val message: TextReference = + resourceReference(id = R.string.send_alert_fee_coverage_title, wrappedList(amount)) override val confirmButtonText: TextReference = resourceReference(id = R.string.send_alert_fee_coverage_subract_text) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt index 490e25e8e1..2928e3d1f6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt @@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.state import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory @@ -45,10 +46,17 @@ internal class SendEventStateFactory( fun getFeeCoverageAlert(onConsume: () -> Unit): SendUiState { val state = currentStateProvider() + val amount = state.feeState?.fee?.amount ?: return state + val feeAmount = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = amount.value, + cryptoCurrency = state.cryptoCurrencySymbol, + decimals = amount.decimals, + ) return state.copy( event = triggeredEvent( data = SendEvent.ShowAlert( SendAlertState.FeeCoverage( + amount = feeAmount, onConfirmClick = clickIntents::onSubtractSelect, ), ), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index 752a235ce0..631191ad66 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -95,6 +95,11 @@ internal sealed class SendNotification(val config: NotificationConfig) { ) }, ) + + data class ExistentialDeposit(val deposit: String) : Error( + title = resourceReference(R.string.send_notification_existential_deposit_title), + subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), + ) } sealed class Warning( @@ -125,11 +130,6 @@ internal sealed class SendNotification(val config: NotificationConfig) { onCloseClick = onCloseClick, ) - data class ExistentialDeposit(val deposit: String) : Warning( - title = resourceReference(R.string.send_notification_existential_deposit_title), - subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), - ) - data object FeeTooLow : Warning( title = resourceReference(id = R.string.send_notification_transaction_delay_title), subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index bb10ce4ab4..d16f511bbd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -105,6 +105,6 @@ enum class SendUiStateType { None, Recipient, Amount, - Fee, Send, + Fee, } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 85402bfc8f..8b5e5530c2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -213,7 +213,7 @@ internal class SendNotificationFactory( ) if (currencyDeposit != null && currencyDeposit > spendingAmount) { add( - SendNotification.Warning.ExistentialDeposit( + SendNotification.Error.ExistentialDeposit( BigDecimalFormatter.formatCryptoAmount( cryptoAmount = currencyDeposit, cryptoCurrency = cryptoCurrency, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt index 3f3d18d3bd..d1cb3b1dca 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt @@ -1,19 +1,14 @@ package com.tangem.features.send.impl.presentation.ui.amount -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.ui.common.notifications import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents -import kotlinx.collections.immutable.ImmutableList @Composable internal fun SendAmountContent( @@ -31,29 +26,4 @@ internal fun SendAmountContent( buttons(amountState.segmentedButtonConfig, clickIntents) notifications(amountState.notifications) } -} - -@OptIn(ExperimentalFoundationApi::class) -internal fun LazyListScope.notifications(configs: ImmutableList, modifier: Modifier = Modifier) { - items( - items = configs, - key = { it::class.java }, - contentType = { it::class.java }, - itemContent = { - val bottomPadding = if (it == configs.last()) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing0 - Notification( - config = it.config, - modifier = modifier - .animateItemPlacement() - .padding( - top = TangemTheme.dimens.spacing12, - bottom = bottomPadding, - ), - containerColor = when (it) { - is SendNotification.Warning.NetworkFeeUnreachable -> TangemTheme.colors.background.action - else -> TangemTheme.colors.button.disabled - }, - ) - }, - ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt new file mode 100644 index 0000000000..21791d730d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt @@ -0,0 +1,58 @@ +package com.tangem.features.send.impl.presentation.ui.common + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.state.SendNotification +import kotlinx.collections.immutable.ImmutableList + +@OptIn(ExperimentalFoundationApi::class) +internal fun LazyListScope.notifications( + notifications: ImmutableList, + modifier: Modifier = Modifier, + hasPaddingAbove: Boolean = false, +) { + itemsIndexed( + items = notifications, + key = { _, item -> item::class.java }, + contentType = { _, item -> item::class.java }, + itemContent = { i, item -> + val bottomPadding = if (i == notifications.lastIndex) { + TangemTheme.dimens.spacing12 + } else { + TangemTheme.dimens.spacing0 + } + val topPadding = if (i == 0 && hasPaddingAbove) { + TangemTheme.dimens.spacing0 + } else { + TangemTheme.dimens.spacing12 + } + Notification( + config = item.config, + modifier = modifier + .padding( + top = topPadding, + bottom = bottomPadding, + ) + .animateItemPlacement(), + containerColor = when (item) { + is SendNotification.Error.ExceedsBalance, + is SendNotification.Warning.NetworkFeeUnreachable, + is SendNotification.Warning.HighFeeError, + -> TangemTheme.colors.background.action + else -> TangemTheme.colors.button.disabled + }, + iconTint = when (item) { + is SendNotification.Error.ExceedsBalance, + is SendNotification.Warning, + -> null + is SendNotification.Error -> TangemTheme.colors.icon.warning + }, + ) + }, + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt index 92d9b44d51..2276dc4848 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt @@ -1,9 +1,10 @@ package com.tangem.features.send.impl.presentation.ui.fee -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.components.inputrow.InputRowEnterAmount @@ -24,6 +25,8 @@ internal fun SendCustomFeeEthereum( AnimatedVisibility( visible = selectedFee == FeeType.Custom && customValues.isNotEmpty(), label = "Custom Fee Selected Animation", + enter = expandVertically().plus(fadeIn()), + exit = shrinkVertically().plus(fadeOut()), ) { Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), @@ -31,8 +34,14 @@ internal fun SendCustomFeeEthereum( ) { repeat(customValues.size) { index -> val value = customValues[index] + val bottomPadding = if (index == customValues.lastIndex) { + TangemTheme.dimens.spacing12 + } else { + TangemTheme.dimens.spacing0 + } FooterContainer( footer = value.footer.resolveReference(), + modifier = Modifier.padding(bottom = bottomPadding), ) { if (value.label != null) { InputRowEnterInfoAmount( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt index c17e9c4458..cb132488c6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -7,16 +7,14 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.ui.common.notifications import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents -import kotlinx.collections.immutable.ImmutableList private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY" private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY" @@ -24,7 +22,8 @@ private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY" @Composable internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) { if (state == null) return - val feeSendState = state.feeSelectorState + val feeSendState = state.feeSelectorState as? FeeSelectorState.Content + val isCustomSelected = feeSendState?.selectedFee == FeeType.Custom val notifications = state.notifications LazyColumn( modifier = Modifier @@ -35,8 +34,10 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S ), ) { feeSelector(state, clickIntents) - customFee(feeSendState) - notifications(notifications) + if (feeSendState != null) { + customFee(feeSendState) + } + notifications(notifications = notifications, hasPaddingAbove = isCustomSelected) } } @@ -54,45 +55,18 @@ private fun LazyListScope.feeSelector(state: SendStates.FeeState, clickIntents: } @OptIn(ExperimentalFoundationApi::class) -private fun LazyListScope.notifications(configs: ImmutableList, modifier: Modifier = Modifier) { - items( - items = configs, - key = { it::class.java }, - contentType = { it::class.java }, - itemContent = { - val bottomPadding = if (it == configs.last()) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing0 - Notification( - config = it.config, - modifier = modifier - .padding( - top = TangemTheme.dimens.spacing12, - bottom = bottomPadding, - ) - .animateItemPlacement(), - containerColor = when (it) { - is SendNotification.Warning.NetworkFeeUnreachable -> TangemTheme.colors.background.action - else -> TangemTheme.colors.button.disabled - }, - ) - }, - ) -} - -@OptIn(ExperimentalFoundationApi::class) -internal fun LazyListScope.customFee(feeSendState: FeeSelectorState, modifier: Modifier = Modifier) { - if (feeSendState is FeeSelectorState.Content) { - item( - key = FEE_CUSTOM_KEY, - ) { - SendCustomFeeEthereum( - customValues = feeSendState.customValues, - selectedFee = feeSendState.selectedFee, - modifier = modifier - .fillMaxWidth() - .animateItemPlacement() - .background(TangemTheme.colors.background.tertiary) - .padding(top = TangemTheme.dimens.spacing12), - ) - } +internal fun LazyListScope.customFee(feeSendState: FeeSelectorState.Content, modifier: Modifier = Modifier) { + item( + key = FEE_CUSTOM_KEY, + ) { + SendCustomFeeEthereum( + customValues = feeSendState.customValues, + selectedFee = feeSendState.selectedFee, + modifier = modifier + .fillMaxWidth() + .animateItemPlacement() + .background(TangemTheme.colors.background.tertiary) + .padding(top = TangemTheme.dimens.spacing12), + ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index 134cea2336..3a31549af5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -61,7 +61,6 @@ internal fun SendRecipientContent(uiState: SendStates.RecipientState?, clickInte isLoading = isValidating, error = address.error, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing4) .background( color = TangemTheme.colors.background.action, shape = TangemTheme.shapes.roundedCornersXMedium, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index c5be1f7b27..78809756c4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.items import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -18,13 +17,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendUiState -import kotlinx.collections.immutable.ImmutableList +import com.tangem.features.send.impl.presentation.ui.common.notifications import kotlinx.coroutines.delay private const val TAP_HELP_KEY = "TAP_HELP_KEY" @@ -39,7 +36,6 @@ internal fun SendContent(uiState: SendUiState) { modifier = Modifier .fillMaxSize() .padding(horizontal = TangemTheme.dimens.spacing16), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { blocks(uiState) tapHelp(isDisplay = sendState.showTapHelp) @@ -109,7 +105,8 @@ private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modif horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier .fillMaxWidth() - .animateItemPlacement(), + .animateItemPlacement() + .padding(top = TangemTheme.dimens.spacing20), ) { val background = TangemTheme.colors.button.secondary Icon( @@ -133,31 +130,4 @@ private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modif } } } -} - -@OptIn(ExperimentalFoundationApi::class) -internal fun LazyListScope.notifications(configs: ImmutableList, modifier: Modifier = Modifier) { - items( - items = configs, - key = { it::class.java }, - contentType = { it::class.java }, - itemContent = { - Notification( - config = it.config, - modifier = modifier.animateItemPlacement(), - containerColor = when (it) { - is SendNotification.Error.ExceedsBalance, - is SendNotification.Warning.HighFeeError, - -> TangemTheme.colors.background.action - else -> TangemTheme.colors.button.disabled - }, - iconTint = when (it) { - is SendNotification.Error.ExceedsBalance, - is SendNotification.Warning, - -> null - is SendNotification.Error -> TangemTheme.colors.icon.warning - }, - ) - }, - ) } \ No newline at end of file From 68c8980624976e3f12ea78e890dae84e3bbe9641 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 31 Mar 2024 13:43:08 +0800 Subject: [PATCH 052/109] Updated on 2026-08-14 --- .../impl/presentation/ui/components/AddCustomTokenForm.kt | 4 ++++ gradle/dependencies.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt index fb6c260728..dd1293ba65 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt @@ -132,6 +132,10 @@ private fun TextField(model: AddCustomTokenInputField, isError: Boolean) { } } +/** + * Be careful with this function – ExposedDropdownMenuBox can crash the app if it is open and user clicks system back + * button. It was fixed in compose-material 1.6.4. + */ @OptIn(ExperimentalMaterialApi::class) @Composable private fun SelectorField(model: AddCustomTokenSelectorField) { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index a8f85f8c06..42119bfeb6 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -27,7 +27,7 @@ androidx-datastore = "1.0.0" compose-compiler = "1.5.9" compose-runtime = "1.6.1" compose-foundation = "1.6.1" -compose-material = "1.6.1" +compose-material = "1.6.4" compose-material3 = "1.2.0" compose-constraint = "1.0.1" compose-navigation = "2.7.7" From db8b6484b3b89e02f18c01eb1ffd0c3e93c2d90f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Apr 2024 15:38:28 +0500 Subject: [PATCH 053/109] Updated on 2026-08-14 --- .../presentation/ui/amount/AmountField.kt | 13 +++--- .../ui/amount/AmountFieldContainer.kt | 1 - .../ui/amount/SendAmountContent.kt | 44 ++++++++++++++++++- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt index e6206028fe..4c45622e33 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -3,8 +3,7 @@ package com.tangem.features.send.impl.presentation.ui.amount import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment.Companion.BottomCenter @@ -22,15 +21,15 @@ import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.send.impl.presentation.state.fields.SendTextField @Composable -internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean, isEnabled: Boolean) { +internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolean) { val decimalFormat = rememberDecimalFormat() - val (primaryValue, secondaryValue) = if (isFiat) { + val (primaryValue, secondaryValue) = if (sendField.isFiatValue) { sendField.fiatValue to sendField.value } else { sendField.value to sendField.fiatValue } - val (primaryAmount, secondaryAmount) = if (isFiat) { + val (primaryAmount, secondaryAmount) = if (sendField.isFiatValue) { sendField.fiatAmount to sendField.cryptoAmount } else { sendField.cryptoAmount to sendField.fiatAmount @@ -48,13 +47,15 @@ internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean, textAlign = TextAlign.Center, ), isEnabled = isEnabled, + isAutoResize = true, placeholderAlignment = TopCenter, modifier = Modifier .padding( top = TangemTheme.dimens.spacing24, start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, - ), + ) + .requiredHeightIn(min = TangemTheme.dimens.size32), ) Box( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt index 836ffd79a8..5cf940b41b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt @@ -62,7 +62,6 @@ internal fun LazyListScope.amountField( ) AmountField( sendField = amountState.amountTextField, - isFiat = amountState.amountTextField.isFiatValue, isEnabled = !amountState.isFeeLoading, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt index 739ee94f29..2156dc6f48 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt @@ -5,8 +5,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData +import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub import com.tangem.features.send.impl.presentation.ui.common.notifications import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents @@ -30,4 +35,41 @@ internal fun SendAmountContent( ) notifications(amountState.notifications) } -} \ No newline at end of file +} + +// region Preview +@Preview +@Composable +private fun AmountFieldPreview_Light( + @PreviewParameter(AmountFieldPreviewProvider::class) amountState: SendStates.AmountState, +) { + TangemTheme { + SendAmountContent( + amountState = amountState, + isBalanceHiding = false, + clickIntents = SendClickIntentsStub, + ) + } +} + +@Preview +@Composable +private fun AmountFieldPreview_Dark( + @PreviewParameter(AmountFieldPreviewProvider::class) amountState: SendStates.AmountState, +) { + TangemTheme(isDark = true) { + SendAmountContent( + amountState = amountState, + isBalanceHiding = false, + clickIntents = SendClickIntentsStub, + ) + } +} + +private class AmountFieldPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + AmountStatePreviewData.amountState, + ) +} +// endregion \ No newline at end of file From 7d02f3e3a6a32de560fb2d789c93bdf1b623cc7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Apr 2024 15:39:11 +0500 Subject: [PATCH 054/109] Updated on 2026-08-14 --- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../presentation/ui/SendNavigationButtons.kt | 8 ++++++-- .../send/impl/presentation/ui/SendScreen.kt | 18 +++++++++++------- .../presentation/ui/common/Notifications.kt | 2 +- .../ui/fee/SendCustomFeeEthereum.kt | 5 +++-- .../ui/fee/SendSpeedAndFeeContent.kt | 12 +++++++++--- .../ui/recipient/SendRecipientContent.kt | 2 +- 7 files changed, 32 insertions(+), 16 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index d835895565..79a2e16273 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -125,6 +125,7 @@ data class TangemDimens internal constructor( val spacing56: Dp = 56.dp, val spacing60: Dp = 60.dp, val spacing70: Dp = 70.dp, + val spacing72: Dp = 72.dp, val spacing76: Dp = 76.dp, val spacing80: Dp = 80.dp, val spacing92: Dp = 92.dp, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 94b544a15e..5bdb10824e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -31,13 +31,17 @@ import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType @Composable -internal fun SendNavigationButtons(uiState: SendUiState, currentState: SendUiCurrentScreen) { +internal fun SendNavigationButtons( + uiState: SendUiState, + currentState: SendUiCurrentScreen, + modifier: Modifier = Modifier, +) { val sendState = uiState.sendState ?: return val isSuccess = sendState.isSuccess val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess val isSentState = currentState.type == SendUiStateType.Send && isSuccess Column( - modifier = Modifier + modifier = modifier .padding( start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing16, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 91c44b07ab..b227fe07fe 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -74,13 +74,17 @@ internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow item::class.java }, itemContent = { i, item -> val bottomPadding = if (i == notifications.lastIndex) { - TangemTheme.dimens.spacing12 + TangemTheme.dimens.spacing72 } else { TangemTheme.dimens.spacing0 } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt index 2276dc4848..50fde69ede 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt @@ -20,6 +20,7 @@ import kotlinx.collections.immutable.ImmutableList internal fun SendCustomFeeEthereum( customValues: ImmutableList, selectedFee: FeeType, + hasNotifications: Boolean, modifier: Modifier = Modifier, ) { AnimatedVisibility( @@ -34,8 +35,8 @@ internal fun SendCustomFeeEthereum( ) { repeat(customValues.size) { index -> val value = customValues[index] - val bottomPadding = if (index == customValues.lastIndex) { - TangemTheme.dimens.spacing12 + val bottomPadding = if (index == customValues.lastIndex && !hasNotifications) { + TangemTheme.dimens.spacing72 } else { TangemTheme.dimens.spacing0 } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt index cb132488c6..ee4055bff5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -23,8 +23,9 @@ private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY" internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) { if (state == null) return val feeSendState = state.feeSelectorState as? FeeSelectorState.Content - val isCustomSelected = feeSendState?.selectedFee == FeeType.Custom val notifications = state.notifications + val isCustomSelected = feeSendState?.selectedFee == FeeType.Custom + val hasNotifications = notifications.isNotEmpty() LazyColumn( modifier = Modifier .fillMaxSize() @@ -35,7 +36,7 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S ) { feeSelector(state, clickIntents) if (feeSendState != null) { - customFee(feeSendState) + customFee(feeSendState = feeSendState, hasNotifications = hasNotifications) } notifications(notifications = notifications, hasPaddingAbove = isCustomSelected) } @@ -55,13 +56,18 @@ private fun LazyListScope.feeSelector(state: SendStates.FeeState, clickIntents: } @OptIn(ExperimentalFoundationApi::class) -internal fun LazyListScope.customFee(feeSendState: FeeSelectorState.Content, modifier: Modifier = Modifier) { +internal fun LazyListScope.customFee( + feeSendState: FeeSelectorState.Content, + hasNotifications: Boolean, + modifier: Modifier = Modifier, +) { item( key = FEE_CUSTOM_KEY, ) { SendCustomFeeEthereum( customValues = feeSendState.customValues, selectedFee = feeSendState.selectedFee, + hasNotifications = hasNotifications, modifier = modifier .fillMaxWidth() .animateItemPlacement() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index 3a31549af5..93910bc2a6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -182,7 +182,7 @@ private fun LazyListScope.listItem( .then( if (isLast && index == list.lastIndex) { Modifier - .padding(bottom = TangemTheme.dimens.spacing12) + .padding(bottom = TangemTheme.dimens.spacing72) .clip( shape = RoundedCornerShape( bottomStart = TangemTheme.dimens.radius16, From aa3ce9484b1b3f5c8decca3daf8d41dd0f517022 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Apr 2024 15:39:17 +0500 Subject: [PATCH 055/109] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 23 +++++++++++++++++++-- core/res/src/main/res/values/strings.xml | 11 +++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index f71126d7f2..bc227501e8 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -368,6 +368,7 @@ По балансу Сортировка токенов Список + Выбрать из галереи Нравится наше приложение? %1$s (%2$s) в сети %3$s Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств. @@ -444,17 +445,23 @@ Низкая Нормальная Приоритетная + Проверьте своё интернет соединение + Информация о комиссии сети недоступна + Из Лимит газа + Это максимальное количество газа, которое будет потрачено на выполнение транзакции или контракта. Лимит газа предотвращает неожиданные или неограниченные расходы при выполнении транзакции. Цена газа - Цена газа влияет на скорость транзакции. При сильно низкой, транзакция может быть не обработана. + Это стоимость, которую вы готовы заплатить за каждую единицу газа. Чем выше цена газа, тем быстрее ваша транзакция будет обработана. Всё Максимальная сумма Комиссия не превысит - Максимальная cумма комиссии + Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение. Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s. Покрытие сетевой комиссии Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств + Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, убедитесь, что остаток после отправки будет не менее %s. + Экзистенциальный депозит Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Установлена высокая комиссия Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s. @@ -471,12 +478,18 @@ Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. Лимит транзакции Необязательное + Пожалуйста, совместите свой QR-код с квадратом, чтобы отсканировать его. Убедитесь, что вы сканируете адрес в сети %s. Последние Получатель + Неверный адрес Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов Отправить Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. Мои кошельки + Отправка + Нажмите на любое поле, чтобы изменить его + Отправка %s + Вы отправляете %1$s, включая комиссию сети %2$s Отправка %s Всего %1$s и %2$s будет отправлено @@ -484,6 +497,7 @@ %s будет отправлено Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время Неверный адрес + Транзакция отправлена Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Революционный аппаратный кошелек До трех карт с одним кошельком @@ -521,6 +535,11 @@ Балансы скрыты Балансы показаны Отменить + В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением.” + У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. + Выбранная операция в данный момент недоступна. Попробуйте позже. + Обмен %s не доступен. Но мы работаем над его добавлением. + В данный момент продажа монеты %s недоступна. Но мы работаем над её добавлением. Сгенерировать XPUB Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8e0afa7d1e..eacae3a8ac 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -444,13 +444,13 @@ Network fee info unreachable From Gas limit - Gas Limit is auto-calculated; raise it during network congestion + This is the maximum amount of gas that will be spent to complete a transaction or contract. A gas limit prevents unexpected or unlimited charges when executing a transaction. Gas price - Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed + This is the cost you are willing to pay for each unit of gas. The higher the gas price, the faster your transaction will be processed. Max Maximum amount Max fee - Maximum fee amount + The fee that will be charged for your transaction. You can set your own value. Numbers only for Destination Tag Sending amount will be reduced by %1$s to cover the selected commission level. The recipient will get %2$s. Network fee coverage @@ -532,6 +532,11 @@ Balances hidden Balances shown Undo + The purchase of the %s is currently unavailable. But we are working on adding it. + You do not have funds to send. Top up your account to be able to send funds from it. + This operation is currently unavailable. Please try again later. + %s swap is not available. But we are working on adding it. + Sell of the %s coin is currently unavailable. But we are working on adding it. Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. From 4df89bb5213ae03dd15720b8a0d6ebe8a9f16c7a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Apr 2024 15:39:08 +0300 Subject: [PATCH 056/109] Updated on 2026-08-14 --- .../com/tangem/datasource/api/express/TangemExpressApi.kt | 2 ++ .../java/com/tangem/feature/swap/DefaultSwapRepository.kt | 4 ++++ .../java/com/tangem/feature/swap/domain/api/SwapRepository.kt | 2 ++ .../java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 2 ++ 4 files changed, 10 insertions(+) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 61c2a6c25a..b7055574a3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -50,6 +50,8 @@ interface TangemExpressApi { @Query("rateType") rateType: String, @Query("toAddress") toAddress: String, @Query("requestId") requestId: String, + @Query("refundAddress") refundAddress: String?, // for cex only + @Query("refundExtraId") refundExtraId: String?, // for cex only ): ApiResponse @GET("exchange-status") diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 5ce1371a15..c67891b454 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -256,6 +256,8 @@ internal class DefaultSwapRepository @Inject constructor( providerId: String, rateType: RateType, toAddress: String, + refundAddress: String?, // for cex only + refundExtraId: String?, // for cex only ): Either { return withContext(coroutineDispatcher.io) { try { @@ -272,6 +274,8 @@ internal class DefaultSwapRepository @Inject constructor( rateType = rateType.name.lowercase(), toAddress = toAddress, requestId = requestId, + refundAddress = refundAddress, + refundExtraId = refundExtraId, ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { val txDetails = parseTxDetails(response.txDetailsJson) diff --git a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index 8fcb79ae26..0a04d8950f 100644 --- a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt +++ b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -65,6 +65,8 @@ interface SwapRepository { providerId: String, rateType: RateType, toAddress: String, + refundAddress: String? = null, // for cex only + refundExtraId: String? = null, // for cex only ): Either fun getNativeTokenForNetwork(networkId: String): CryptoCurrency diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 6df1eadf9d..08b31e9a2e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -589,6 +589,8 @@ internal class SwapInteractorImpl @Inject constructor( providerId = swapProvider.providerId, rateType = RateType.FLOAT, toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value ?: "", + refundAddress = currencyToSend.value.networkAddress?.defaultAddress?.value, + refundExtraId = null, // currently always null ).getOrElse { return SwapTransactionState.ExpressError(it) } val exchangeDataCex = From fcfbcf283f063a633117429f7b074cfebafdf785 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Apr 2024 17:49:57 +0500 Subject: [PATCH 057/109] Updated on 2026-08-14 --- .../send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index 2caaa43e1b..3ecc6ddf2e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -163,11 +163,11 @@ private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? { private fun FeeSelectorState.Content?.getDividerAndVisibility(feeType: FeeType): Pair { val hasCustomValues = !this?.customValues.isNullOrEmpty() val isNotSingle = this?.fees !is TransactionFee.Single - val isSingle = this?.fees is TransactionFee.Single + val isLoaded = this?.fees != null return when (feeType) { FeeType.Slow -> true to isNotSingle - FeeType.Market -> isNotSingle to isSingle + FeeType.Market -> isNotSingle to isLoaded FeeType.Fast -> hasCustomValues to isNotSingle - FeeType.Custom -> false to isNotSingle + FeeType.Custom -> false to hasCustomValues } } \ No newline at end of file From cf5a7d68ad51af2c2105118d1dfce8564bfa8fb2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Mar 2024 19:56:01 +0000 Subject: [PATCH 058/109] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 2 +- .../com/tangem/tap/domain/TangemSdkManager.kt | 2 + .../tasks/product/CreateProductWalletTask.kt | 6 +- .../domain/tasks/product/CreateWalletsTask.kt | 3 +- .../wallet/redux/OnboardingWalletAction.kt | 1 + .../redux/OnboardingWalletMiddleware.kt | 1 + .../wallet/ui/OnboardingWalletFragment.kt | 10 +- .../tangem/core/ui/components/TextFields.kt | 123 ++++++++++++++++++ .../model/OnboardingSeedPhraseState.kt | 14 +- .../presentation/wallet2/model/UiActions.kt | 2 + .../wallet2/ui/ImportSeedPhraseScreen.kt | 119 +++++++++++------ .../wallet2/ui/PassphraseInfoBottomSheet.kt | 89 +++++++++++++ .../ImportSeedPhraseStateBuilder.kt | 46 ++++++- .../wallet2/ui/stateBuiders/StateBuilder.kt | 7 +- .../wallet2/viewmodel/SeedPhraseMediator.kt | 1 + .../wallet2/viewmodel/SeedPhraseViewModel.kt | 46 +++++-- 16 files changed, 406 insertions(+), 66 deletions(-) create mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index c1d5853c80..d5d7000089 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -407,7 +407,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0 val isNotScannedBefore = store.state.globalState.scanResponse == null - val isOnboardingServiceNotActive = store.state.globalState.onboardingState.onboardingStarted + val isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted when { !backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> { diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 548f91c4ad..4097061289 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -106,6 +106,7 @@ class TangemSdkManager( suspend fun importWallet( scanResponse: ScanResponse, mnemonic: String, + passphrase: String?, shouldReset: Boolean, ): CompletionResult { val defaultMnemonic = try { @@ -118,6 +119,7 @@ class TangemSdkManager( cardTypesResolver = scanResponse.cardTypesResolver, derivationStyleProvider = scanResponse.derivationStyleProvider, mnemonic = defaultMnemonic, + passphrase = passphrase, shouldReset = shouldReset, ), scanResponse.card.cardId, diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index f5c1a21f38..699e7a527e 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -62,6 +62,7 @@ class CreateProductWalletTask( private val cardTypesResolver: CardTypesResolver, private val derivationStyleProvider: DerivationStyleProvider, private val mnemonic: Mnemonic? = null, + private val passphrase: String? = null, private val shouldReset: Boolean, ) : CardSessionRunnable { @@ -82,7 +83,7 @@ class CreateProductWalletTask( cardTypesResolver.isTangemTwins() -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet") - else -> CreateWalletTangemWallet(mnemonic, shouldReset, derivationStyleProvider, cardDto) + else -> CreateWalletTangemWallet(mnemonic, passphrase, shouldReset, derivationStyleProvider, cardDto) } commandProcessor.proceed(cardDto, session) { when (it) { @@ -142,6 +143,7 @@ private class CreateWalletTangemNote(private val cardTypesResolver: CardTypesRes */ private class CreateWalletTangemWallet( private val mnemonic: Mnemonic?, + private val passphrase: String?, private val shouldReset: Boolean, private val derivationStyleProvider: DerivationStyleProvider, cardDTO: CardDTO, @@ -170,7 +172,7 @@ private class CreateWalletTangemWallet( session: CardSession, callback: (result: CompletionResult) -> Unit, ) { - CreateWalletsTask(cardConfig.mandatoryCurves, mnemonic).run(session) { result -> + CreateWalletsTask(cardConfig.mandatoryCurves, mnemonic, passphrase).run(session) { result -> when (result) { is CompletionResult.Success -> { checkIfAllWalletsCreated(card, session, result.data, callback) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt index 2c75d8da36..c0ddcdeea3 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt @@ -21,6 +21,7 @@ class CreateWalletsResponse( class CreateWalletsTask( private val curves: List, private val mnemonic: Mnemonic? = null, + private val passphrase: String?, ) : CardSessionRunnable { private val createdWalletsResponses = mutableListOf() @@ -41,7 +42,7 @@ class CreateWalletsTask( callback: (result: CompletionResult) -> Unit, ) { val extendedPrivateKey = mnemonic?.let { - AnyMasterKeyFactory(mnemonic = it, passphrase = "").makeMasterKey(curve) + AnyMasterKeyFactory(mnemonic = it, passphrase = passphrase ?: "").makeMasterKey(curve) } CreateWalletTask(curve, extendedPrivateKey).run(session) { result -> when (result) { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt index 5bd2df0285..c624e36825 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt @@ -38,6 +38,7 @@ sealed class OnboardingWallet2Action : OnboardingWalletAction() { data class ImportWallet( val mnemonicComponents: List, + val passphrase: String?, val seedPhraseSource: SeedPhraseSource, val callback: (CompletionResult) -> Unit, ) : OnboardingWallet2Action() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 16045b38c8..5c29c96fb9 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -271,6 +271,7 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { val mediateResult = when ( val result = tangemSdkManager.importWallet( scanResponse = scanResponse, + passphrase = action.passphrase, mnemonic = action.mnemonicComponents.joinToString(" "), shouldReset = globalState.onboardingState.shouldResetOnCreate, ) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 0655d01aa9..deb5bb4873 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -523,10 +523,18 @@ class OnboardingWalletFragment : override fun importWallet( mnemonicComponents: List, + passphrase: String?, seedPhraseSource: SeedPhraseSource, callback: (CompletionResult) -> Unit, ) { - store.dispatch(OnboardingWallet2Action.ImportWallet(mnemonicComponents, seedPhraseSource, callback)) + store.dispatch( + OnboardingWallet2Action.ImportWallet( + mnemonicComponents, + passphrase, + seedPhraseSource, + callback, + ), + ) } override fun allowScreenshots(allow: Boolean) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt index e954cbd32d..f8312aa659 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween @@ -59,6 +60,40 @@ fun OutlineTextField( ) } +@Composable +fun OutlineTextFieldWithIcon( + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + @DrawableRes iconResId: Int, + iconColor: Color, + onIconClick: () -> Unit, + modifier: Modifier = Modifier, + label: String? = null, + placeholder: String? = null, + enabled: Boolean = true, + isError: Boolean = false, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + TangemTextFieldWithIcon( + modifier = modifier, + value = value, + onValueChange = onValueChange, + iconResId = iconResId, + iconColor = iconColor, + singleLine = true, + label = label, + placeholder = placeholder, + enabled = enabled, + isError = isError, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + interactionSource = interactionSource, + onIconClick = onIconClick, + ) +} + // region Defaults @Suppress("LongMethod") @Composable @@ -168,6 +203,84 @@ private fun TangemTextField( } } +@Suppress("LongMethod") +@Composable +private fun TangemTextFieldWithIcon( + value: TextFieldValue, + singleLine: Boolean, + onValueChange: (TextFieldValue) -> Unit, + @DrawableRes iconResId: Int, + iconColor: Color, + modifier: Modifier = Modifier, + label: String? = null, + placeholder: String? = null, + enabled: Boolean = true, + isError: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + colors: TangemTextFieldColors = TangemTextFieldsDefault.defaultTextFieldColors, + size: TangemTextFieldSize = TangemTextFieldSize.Default, + onIconClick: () -> Unit = {}, +) { + Column(modifier = modifier) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .heightIn(size.toHeightDp()), + value = value, + textStyle = TangemTheme.typography.body1, + onValueChange = onValueChange, + singleLine = singleLine, + enabled = enabled, + isError = isError, + visualTransformation = visualTransformation, + keyboardActions = keyboardActions, + keyboardOptions = keyboardOptions, + interactionSource = interactionSource, + shape = size.toShape(), + colors = colors, + label = { + if (!label.isNullOrEmpty()) { + Text( + text = label, + style = TangemTheme.typography.caption2, + color = colors.labelColor( + enabled = enabled, + error = isError, + interactionSource = interactionSource, + ).value, + ) + } + }, + placeholder = { + if (!placeholder.isNullOrEmpty()) { + Text( + text = placeholder, + style = TangemTheme.typography.body1, + color = colors.placeholderColor(enabled = enabled).value, + ) + } + }, + trailingIcon = { + IconButton( + modifier = Modifier.size(32.dp), + onClick = onIconClick, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(id = iconResId), + tint = iconColor, + contentDescription = "Clear input", + ) + } + }, + ) + } +} + private enum class TangemTextFieldSize { Default, } @@ -378,6 +491,16 @@ private fun OutlineTextFieldSample(modifier: Modifier = Modifier) { caption = "Supporting text", ) Divider(modifier = Modifier.padding(vertical = 8.dp)) + OutlineTextFieldWithIcon( + value = TextFieldValue(), + onValueChange = { /* no-op */ }, + iconResId = R.drawable.ic_alert_24, + iconColor = TangemTheme.colors.icon.informative, + label = "Default without value", + placeholder = "Placeholder", + onIconClick = {}, + ) + Divider(modifier = Modifier.padding(vertical = 8.dp)) } } diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt index 5591aa1013..a8300c9a76 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt @@ -1,6 +1,8 @@ package com.tangem.feature.onboarding.presentation.wallet2.model import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.feature.onboarding.domain.SeedPhraseError import com.tangem.feature.onboarding.domain.models.MnemonicType import kotlinx.collections.immutable.ImmutableList @@ -64,11 +66,6 @@ enum class SegmentSeedType(val count: Int) { } } -data class MnemonicState( - val seedType: SegmentSeedType, - val mnemonicItems: PersistentList, -) - data class MnemonicGridItem( val index: Int, val mnemonic: String, @@ -82,14 +79,19 @@ data class CheckSeedPhraseState( ) data class ImportSeedPhraseState( - val tvSeedPhrase: TextFieldState, + val fieldSeedPhrase: TextFieldState, + val fieldPassphrase: TextFieldState, val onSuggestedPhraseClick: (Int) -> Unit, + val onPassphraseInfoClick: () -> Unit, val buttonCreateWallet: ButtonState, val invalidWords: Set = emptySet(), val suggestionsList: ImmutableList = persistentListOf(), val error: SeedPhraseError? = null, + val bottomSheetConfig: TangemBottomSheetConfig? = null, ) +data class ShowPassphraseInfoBottomSheetContent(val onOkClick: () -> Unit) : TangemBottomSheetConfigContent + data class TextFieldState( val onTextFieldValueChanged: (TextFieldValue) -> Unit, val textFieldValue: TextFieldValue = TextFieldValue(), diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt index afb379bdca..91db0f6407 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt @@ -40,7 +40,9 @@ data class CheckSeedPhraseUiAction( data class ImportSeedPhraseUiAction( val phraseTextFieldAction: TextFieldUiAction, + val passTextFieldAction: TextFieldUiAction, val suggestedPhraseClick: (Int) -> Unit, + val onPassphraseInfoClick: () -> Unit, val buttonCreateWalletClick: () -> Unit, ) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt index e0a5b674ad..abc2bfff70 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt @@ -1,13 +1,16 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui import androidx.compose.animation.* +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.material.OutlinedTextField import androidx.compose.material.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.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -17,9 +20,7 @@ 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.unit.IntOffset -import com.tangem.core.ui.components.Notifier -import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.components.TangemTextFieldsDefault +import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.R import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState @@ -38,33 +39,31 @@ import kotlinx.collections.immutable.persistentListOf */ @Composable fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Modifier) { - Column( + val keyboard by keyboardAsState() + Box( modifier = modifier.fillMaxSize(), ) { - Box( - modifier = Modifier.weight(1f), - ) { - Column { - OnboardingDescriptionBlock( - modifier = Modifier.padding(top = TangemTheme.dimens.size16), - ) { - DescriptionSubTitleText(text = stringResource(id = R.string.onboarding_seed_import_message)) - } - PhraseBlock( - modifier = Modifier - .padding(top = TangemTheme.dimens.size62) - .padding(horizontal = TangemTheme.dimens.size16), - state = state, - ) - SuggestionsBlock( - modifier = Modifier.padding(top = TangemTheme.dimens.size4), - suggestionsList = state.suggestionsList, - onClick = { index -> state.onSuggestedPhraseClick(index) }, - ) + Column { + OnboardingDescriptionBlock( + modifier = Modifier.padding(top = TangemTheme.dimens.size16), + ) { + DescriptionSubTitleText(text = stringResource(id = R.string.onboarding_seed_import_message)) } - } - Box { + PhraseBlock( + modifier = Modifier + .padding(top = TangemTheme.dimens.size62) + .padding(horizontal = TangemTheme.dimens.size16), + state = state, + ) + + PassphraseBlock( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.size16), + passphraseField = state.fieldPassphrase, + onPassphraseInfoClick = state.onPassphraseInfoClick, + ) + OnboardingActionBlock( + modifier = Modifier.padding(top = TangemTheme.dimens.size16), firstActionContent = { PrimaryButtonIconEnd( modifier = Modifier @@ -78,9 +77,41 @@ fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Mo }, ) } + + if (keyboard is Keyboard.Opened) { + SuggestionsBlock( + modifier = Modifier + .align(Alignment.BottomCenter) + .imePadding(), + suggestionsList = state.suggestionsList, + onClick = { index -> state.onSuggestedPhraseClick(index) }, + ) + } + + if (state.bottomSheetConfig != null) { + PassphraseInfoBottomSheet(config = state.bottomSheetConfig) + } } } +@Composable +private fun PassphraseBlock( + passphraseField: TextFieldState, + onPassphraseInfoClick: () -> Unit, + modifier: Modifier = Modifier, +) { + OutlineTextFieldWithIcon( + modifier = modifier.fillMaxWidth(), + value = passphraseField.textFieldValue, + onValueChange = passphraseField.onTextFieldValueChanged, + iconResId = R.drawable.ic_information_24, + iconColor = TangemTheme.colors.icon.informative, + label = stringResource(id = R.string.common_passphrase), + placeholder = stringResource(id = R.string.send_optional_field), + onIconClick = onPassphraseInfoClick, + ) +} + @Composable private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) { // TODO: replace by TextReference @@ -93,8 +124,8 @@ private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modif modifier = Modifier .fillMaxWidth() .height(TangemTheme.dimens.size142), - value = state.tvSeedPhrase.textFieldValue, - onValueChange = state.tvSeedPhrase.onTextFieldValueChanged, + value = state.fieldSeedPhrase.textFieldValue, + onValueChange = state.fieldSeedPhrase.onTextFieldValueChanged, textStyle = TangemTheme.typography.body1, singleLine = false, visualTransformation = InvalidWordsColorTransformation( @@ -131,12 +162,12 @@ private fun SuggestionsBlock( modifier: Modifier = Modifier, ) { AnimatedVisibility( + modifier = modifier, enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }), exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(), visible = suggestionsList.isNotEmpty(), ) { LazyRow( - modifier = modifier, contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16), ) { items(suggestionsList.size) { index -> @@ -160,7 +191,7 @@ private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifi Box(modifier = modifier.clickable(onClick = onClick)) { Notifier( text = text, - backgroundColor = TangemTheme.colors.background.action, + backgroundColor = TangemTheme.colors.icon.primary1, textColor = TangemTheme.colors.text.primary2, ) } @@ -204,19 +235,23 @@ private fun ImportSeedPhraseScreenPreview_Light( @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, ) { TangemTheme(isDark = false) { - ImportSeedPhraseScreen( - ImportSeedPhraseState( - tvSeedPhrase = TextFieldState(onTextFieldValueChanged = {}), - onSuggestedPhraseClick = {}, - buttonCreateWallet = ButtonState( - enabled = true, - isClickable = true, - showProgress = false, - onClick = {}, + Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) { + ImportSeedPhraseScreen( + ImportSeedPhraseState( + fieldSeedPhrase = TextFieldState(onTextFieldValueChanged = {}), + fieldPassphrase = TextFieldState(onTextFieldValueChanged = {}), + onSuggestedPhraseClick = {}, + onPassphraseInfoClick = {}, + buttonCreateWallet = ButtonState( + enabled = true, + isClickable = true, + showProgress = false, + onClick = {}, + ), + suggestionsList = suggestions, ), - suggestionsList = suggestions, - ), - ) + ) + } } } diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt new file mode 100644 index 0000000000..ee04bfe228 --- /dev/null +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt @@ -0,0 +1,89 @@ +package com.tangem.feature.onboarding.presentation.wallet2.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.onboarding.R +import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent + +@Composable +fun PassphraseInfoBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.primary, + ) { content: ShowPassphraseInfoBottomSheetContent -> + PassphraseInfoBottomSheetContent(content) + } +} + +@Composable +fun PassphraseInfoBottomSheetContent(content: ShowPassphraseInfoBottomSheetContent) { + Column( + modifier = Modifier + .background(color = TangemTheme.colors.background.primary) + .fillMaxWidth(), + ) { + Icon( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(top = TangemTheme.dimens.size40) + .size(TangemTheme.dimens.size48), + painter = painterResource(id = R.drawable.ic_information_24), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + + Text( + text = stringResource(id = R.string.common_passphrase), + modifier = Modifier + .padding(top = TangemTheme.dimens.size40) + .align(Alignment.CenterHorizontally), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h2, + ) + + Text( + text = stringResource(id = R.string.onboarding_bottom_sheet_passphrase_description), + modifier = Modifier + .padding(top = TangemTheme.dimens.size16) + .padding(horizontal = TangemTheme.dimens.size24) + .align(Alignment.CenterHorizontally), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + textAlign = TextAlign.Center, + ) + + PrimaryButton( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.size16) + .padding(top = TangemTheme.dimens.size40) + .padding(bottom = TangemTheme.dimens.size32) + .fillMaxWidth(), + text = stringResource(id = R.string.common_ok), + onClick = content.onOkClick, + ) + } +} + +@Preview +@Composable +private fun PassphraseInfoBottomSheetContentPreview() { + TangemTheme { + PassphraseInfoBottomSheetContent(ShowPassphraseInfoBottomSheetContent { }) + } +} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt index 794ec1d599..56169d7fa1 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt @@ -2,9 +2,11 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.feature.onboarding.domain.InsertSuggestionResult import com.tangem.feature.onboarding.domain.SeedPhraseError import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState +import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent import kotlinx.collections.immutable.ImmutableList /** @@ -12,12 +14,23 @@ import kotlinx.collections.immutable.ImmutableList */ class ImportSeedPhraseStateBuilder { - fun updateTextField( + fun updateSeedPhraseTextField( uiState: OnboardingSeedPhraseState, textFieldValue: TextFieldValue, ): OnboardingSeedPhraseState = uiState.copy( importSeedPhraseState = uiState.importSeedPhraseState.copy( - tvSeedPhrase = uiState.importSeedPhraseState.tvSeedPhrase.copy( + fieldSeedPhrase = uiState.importSeedPhraseState.fieldSeedPhrase.copy( + textFieldValue = textFieldValue, + ), + ), + ) + + fun updatePassPhraseTextField( + uiState: OnboardingSeedPhraseState, + textFieldValue: TextFieldValue, + ): OnboardingSeedPhraseState = uiState.copy( + importSeedPhraseState = uiState.importSeedPhraseState.copy( + fieldPassphrase = uiState.importSeedPhraseState.fieldPassphrase.copy( textFieldValue = textFieldValue, ), ), @@ -49,15 +62,40 @@ class ImportSeedPhraseStateBuilder { ), ) + fun showPassphraseInfoBottomSheet( + uiState: OnboardingSeedPhraseState, + onDismiss: () -> Unit, + ): OnboardingSeedPhraseState { + return uiState.copy( + importSeedPhraseState = uiState.importSeedPhraseState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = onDismiss, + content = ShowPassphraseInfoBottomSheetContent(onDismiss), + ), + ), + ) + } + + fun dismissPassphraseBottomSheet(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState { + return uiState.copy( + importSeedPhraseState = uiState.importSeedPhraseState.copy( + bottomSheetConfig = uiState.importSeedPhraseState.bottomSheetConfig?.copy( + isShow = false, + ), + ), + ) + } + fun insertSuggestionWord( uiState: OnboardingSeedPhraseState, insertResult: InsertSuggestionResult, ): OnboardingSeedPhraseState { - val newTextFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue.copy( + val newTextFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue.copy( text = insertResult.text, selection = TextRange(insertResult.cursorPosition, insertResult.cursorPosition), ) - return updateTextField(uiState, newTextFieldValue) + return updateSeedPhraseTextField(uiState, newTextFieldValue) } fun updateError(uiState: OnboardingSeedPhraseState, error: SeedPhraseError?): OnboardingSeedPhraseState { diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt index 749d4be6a0..3a27500c46 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt @@ -28,11 +28,16 @@ class StateBuilder( private fun getInitialImportSeedPhraseState(): ImportSeedPhraseState { return ImportSeedPhraseState( - tvSeedPhrase = TextFieldState( + fieldSeedPhrase = TextFieldState( onTextFieldValueChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onTextFieldChanged, onFocusChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onFocusChanged, ), + fieldPassphrase = TextFieldState( + onTextFieldValueChanged = uiActions.importSeedPhraseActions.passTextFieldAction.onTextFieldChanged, + onFocusChanged = uiActions.importSeedPhraseActions.passTextFieldAction.onFocusChanged, + ), onSuggestedPhraseClick = uiActions.importSeedPhraseActions.suggestedPhraseClick, + onPassphraseInfoClick = uiActions.importSeedPhraseActions.onPassphraseInfoClick, buttonCreateWallet = ButtonState( enabled = false, onClick = uiActions.importSeedPhraseActions.buttonCreateWalletClick, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseMediator.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseMediator.kt index daf7815eb8..5848187991 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseMediator.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseMediator.kt @@ -11,6 +11,7 @@ interface SeedPhraseMediator { fun createWallet(callback: (CompletionResult) -> Unit) fun importWallet( mnemonicComponents: List, + passphrase: String?, seedPhraseSource: SeedPhraseSource, callback: (CompletionResult) -> Unit, ) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt index ed20db4319..74dba8d22c 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt @@ -74,6 +74,7 @@ class SeedPhraseViewModel @Inject constructor( private var generatedMnemonicComponents: Map> = emptyMap() private var importedMnemonicComponents: List? = null + private var importedPassphrase: String? = null override fun onCleared() { textFieldsDebouncers.forEach { entry -> entry.value.release() } @@ -143,7 +144,7 @@ class SeedPhraseViewModel @Inject constructor( buttonCreateWalletClick = { val mnemonicComponents = generatedMnemonicComponents[selectedMnemonicType] ?: return@CheckSeedPhraseUiAction - buttonImportWalletClick(mnemonicComponents, SeedPhraseSource.GENERATED) + buttonImportWalletClick(mnemonicComponents, null, SeedPhraseSource.GENERATED) }, secondTextFieldAction = TextFieldUiAction( onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Second, value) }, @@ -159,10 +160,14 @@ class SeedPhraseViewModel @Inject constructor( phraseTextFieldAction = TextFieldUiAction( onTextFieldChanged = { value -> onSeedPhraseTextFieldChanged(value) }, ), + passTextFieldAction = TextFieldUiAction( + onTextFieldChanged = { value -> onPassPhraseTextFieldChanged(value) }, + ), suggestedPhraseClick = ::buttonSuggestedPhraseClick, + onPassphraseInfoClick = ::buttonPassphraseInfoClick, buttonCreateWalletClick = { analyticsEventHandler.send(SeedPhraseEvents.ButtonImport) - buttonImportWalletClick(importedMnemonicComponents, SeedPhraseSource.IMPORTED) + buttonImportWalletClick(importedMnemonicComponents, importedPassphrase, SeedPhraseSource.IMPORTED) }, ), menuChatClick = ::menuChatClick, @@ -208,13 +213,13 @@ class SeedPhraseViewModel @Inject constructor( // region ImportSeedPhrase private fun onSeedPhraseTextFieldChanged(textFieldValue: TextFieldValue) { - val oldTextFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue + val oldTextFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue val isSameText = textFieldValue.text == oldTextFieldValue.text val isCursorMoved = textFieldValue.selection != oldTextFieldValue.selection - uiState = uiBuilder.importSeedPhrase.updateTextField(uiState, textFieldValue) + uiState = uiBuilder.importSeedPhrase.updateSeedPhraseTextField(uiState, textFieldValue) - val fieldState = uiState.importSeedPhraseState.tvSeedPhrase + val fieldState = uiState.importSeedPhraseState.fieldSeedPhrase val inputMnemonic = fieldState.textFieldValue.text val debouncer = createOrGetDebouncer(MNEMONIC_DEBOUNCER) @@ -236,6 +241,11 @@ class SeedPhraseViewModel @Inject constructor( } } + private fun onPassPhraseTextFieldChanged(textFieldValue: TextFieldValue) { + importedPassphrase = textFieldValue.text + uiState = uiBuilder.importSeedPhrase.updatePassPhraseTextField(uiState, textFieldValue) + } + private suspend fun validateMnemonic(inputMnemonic: String) { if (inputMnemonic.isEmpty() && uiState.importSeedPhraseState.error != null) { val mediateState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = false) @@ -284,11 +294,15 @@ class SeedPhraseViewModel @Inject constructor( } } - private fun buttonImportWalletClick(mnemonicComponents: List?, seedPhraseSource: SeedPhraseSource) { + private fun buttonImportWalletClick( + mnemonicComponents: List?, + passphrase: String?, + seedPhraseSource: SeedPhraseSource, + ) { mnemonicComponents ?: return viewModelScope.launch(dispatchers.io) { - mediator.importWallet(mnemonicComponents, seedPhraseSource, ::handleWalletCreationResult) + mediator.importWallet(mnemonicComponents, passphrase, seedPhraseSource, ::handleWalletCreationResult) } } @@ -391,7 +405,7 @@ class SeedPhraseViewModel @Inject constructor( private fun buttonSuggestedPhraseClick(suggestionIndex: Int) { launchSingle { - val textFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue + val textFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue val word = uiState.importSeedPhraseState.suggestionsList[suggestionIndex] val cursorPosition = textFieldValue.selection.end @@ -406,6 +420,22 @@ class SeedPhraseViewModel @Inject constructor( } } } + + private fun buttonPassphraseInfoClick() { + launchSingle { + updateUi { + uiBuilder.importSeedPhrase.showPassphraseInfoBottomSheet(uiState) { + dismissPassphraseBottomSheet() + } + } + } + } + + private fun dismissPassphraseBottomSheet() { + launchSingle { + updateUi { uiBuilder.importSeedPhrase.dismissPassphraseBottomSheet(uiState) } + } + } // endregion ButtonClickHandlers // region Utils From b99dd44c2bf80a9a6745957701c33ae5df76de41 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Apr 2024 18:33:18 +0800 Subject: [PATCH 059/109] Updated on 2026-08-14 --- .../data/DefaultWalletConnectRepository.kt | 17 +++++++++++++++++ .../domain/models/WalletConnectError.kt | 3 +++ 2 files changed, 20 insertions(+) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt index 46577399e9..a4c78ad4b9 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt @@ -96,6 +96,18 @@ internal class DefaultWalletConnectRepository( Timber.d("sessionProposal: $sessionProposal") this@DefaultWalletConnectRepository.sessionProposal = sessionProposal + if (sessionProposal.name in unsupportedDApps) { + Timber.w("Unsupported DApp") + scope.launch { + _events.emit( + WalletConnectEvents.SessionApprovalError( + WalletConnectError.UnsupportedDApp, + ), + ) + } + return + } + val missingNetworks = findMissingNetworks( namespaces = sessionProposal.requiredNamespaces, userNamespaces = this@DefaultWalletConnectRepository.userNamespaces ?: emptyMap(), @@ -476,4 +488,9 @@ internal class DefaultWalletConnectRepository( val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } } return wcProvidedChains.intersect(userChains.toSet()) } + + private companion object { + + val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro") + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt index efb5215bd7..7d6504d923 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt @@ -1,6 +1,9 @@ package com.tangem.tap.domain.walletconnect2.domain.models sealed class WalletConnectError(val error: String) : Exception() { + + data object UnsupportedDApp : WalletConnectError("UnsupportedDApp") + data class ApprovalErrorMissingNetworks( val missingChains: List, ) : WalletConnectError("ApprovalErrorMissingNetworks") From 06603181c9f93a8a79f114e563aecab3abad2195 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Apr 2024 18:01:19 +0500 Subject: [PATCH 060/109] Updated on 2026-08-14 --- .../send/impl/presentation/state/SendNotification.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index 631191ad66..e59bd0e517 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -36,7 +36,10 @@ internal sealed class SendNotification(val config: NotificationConfig) { data class MinimumAmountError(val amount: String) : Error( title = resourceReference(R.string.send_notification_invalid_amount_title), - subtitle = resourceReference(R.string.send_notification_invalid_minimum_amount_text, wrappedList(amount)), + subtitle = resourceReference( + R.string.send_notification_invalid_minimum_amount_text, + wrappedList(amount, amount), + ), ) data class ReserveAmountError(val amount: String) : Error( From 9ac2009c89488ec0da564ea41655dbfdd6cf83db Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Apr 2024 21:07:38 +0500 Subject: [PATCH 061/109] Updated on 2026-08-14 --- .../AppBarWithBackButtonAndIconContent.kt | 13 +- .../ui/components/fields/AmountTextField.kt | 57 ++------ .../AmountVisualTransformation.kt | 58 ++++++-- .../inputrow/InputRowEnterAmount.kt | 8 +- .../inputrow/InputRowEnterInfoAmount.kt | 15 +- .../core/ui/utils/BigDecimalFormatter.kt | 24 ++++ .../impl/presentation/state/SendUiState.kt | 1 + .../state/amount/SendAmountStateConverter.kt | 1 + .../fee/custom/EthereumCustomFeeConverter.kt | 131 +++++++++++------- .../fields/SendAmountFieldChangeConverter.kt | 8 +- .../previewdata/AmountStatePreviewData.kt | 1 + .../presentation/ui/amount/AmountField.kt | 64 ++++++--- .../ui/amount/AmountFieldContainer.kt | 1 + .../impl/presentation/ui/send/AmountBlock.kt | 17 ++- 14 files changed, 250 insertions(+), 149 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt index 2bd0141ea1..09b6158abb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt @@ -1,10 +1,7 @@ package com.tangem.core.ui.components.appbar import androidx.annotation.DrawableRes -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut +import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -66,8 +63,7 @@ fun AppBarWithBackButtonAndIconContent( ) Column( verticalArrangement = Arrangement.Center, - modifier = Modifier.weight(1f) - .animateContentSize(), + modifier = Modifier.weight(1f), ) { AnimatedVisibility( visible = !text.isNullOrBlank(), @@ -84,8 +80,8 @@ fun AppBarWithBackButtonAndIconContent( } AnimatedVisibility( visible = !subtitle.isNullOrBlank(), - enter = fadeIn(), - exit = fadeOut(), + enter = fadeIn().plus(expandVertically()), + exit = fadeOut().plus(shrinkVertically()), label = "Toolbar subtitle change", ) { Text( @@ -93,6 +89,7 @@ fun AppBarWithBackButtonAndIconContent( color = TangemTheme.colors.text.secondary, maxLines = 1, style = TangemTheme.typography.caption2, + modifier = Modifier.animateContentSize(), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index fb140e00d4..75836f873a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -2,12 +2,10 @@ package com.tangem.core.ui.components.fields import androidx.annotation.FloatRange import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.TopCenter @@ -21,6 +19,7 @@ import androidx.compose.ui.text.ParagraphIntrinsics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.createFontFamilyResolver import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.tooling.preview.Preview @@ -33,20 +32,21 @@ import java.text.DecimalFormat /** * Simple text field for amount input. - * Validates and trims input text using [DecimalFormat]. Formats visual output using [AmountVisualTransformation]. - * Can display aligned placeholder and currency symbol [symbol]. + * Validates and trims input text using [DecimalFormat]. Formats visual output using [visualTransformation]. + * Can display aligned placeholder. * * @param value initial text * @param decimals number of decimal places * @param onValueChange callback * @param textStyle text and placeholder styles * @param modifier modifier - * @param symbol currency symbol * @param color text color - * @param placeholderAlignment alignment of placeholder - * @param showPlaceholder show placeholder + * @param visualTransformation text visual transformation * @param keyboardOptions keyboard options - * + * @param keyboardActions keyboard actions + * @param isEnabled is field editing enabled + * @param isAutoResize is text font auto resize + * @param reduceFactor font resize factor * @see [SimpleTextField] for standard text field */ @Composable @@ -56,10 +56,8 @@ fun AmountTextField( onValueChange: (String) -> Unit, textStyle: TextStyle, modifier: Modifier = Modifier, - symbol: String? = null, color: Color = TangemTheme.colors.text.primary1, - placeholderAlignment: Alignment = TopStart, - showPlaceholder: Boolean = true, + visualTransformation: VisualTransformation = AmountVisualTransformation(decimals), keyboardOptions: KeyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number, ), @@ -70,12 +68,6 @@ fun AmountTextField( reduceFactor: Double = 0.9, ) { val decimalFormat = rememberDecimalFormat() - val visualTransformation = remember { AmountVisualTransformation(decimals, symbol, decimalFormat) } - val placeholderTextAlign = if (placeholderAlignment == TopCenter) { - TextAlign.Center - } else { - TextAlign.Start - } BoxWithConstraints(modifier = modifier) { var fontSize = textStyle.fontSize if (isAutoResize) { @@ -96,6 +88,7 @@ fun AmountTextField( } } } + val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color SimpleTextField( value = value, onValueChange = { newText -> @@ -108,31 +101,13 @@ fun AmountTextField( fontSize = fontSize, textDirection = TextDirection.ContentOrLtr, ), - color = color, + color = textColor, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, singleLine = true, readOnly = !isEnabled, - visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat), + visualTransformation = visualTransformation, modifier = Modifier.background(TangemTheme.colors.background.action), - decorationBox = { innerTextField -> - Box { - if (value.isBlank() && showPlaceholder) { - var placeholder = decimalFormat.defaultFormat() - if (symbol != null) { - placeholder = placeholder.plus(" $symbol") - } - Text( - text = placeholder, - style = textStyle.copy(textDirection = TextDirection.ContentOrLtr), - color = TangemTheme.colors.text.disabled, - textAlign = placeholderTextAlign, - modifier = Modifier.align(placeholderAlignment), - ) - } - innerTextField() - } - }, ) } } @@ -153,9 +128,6 @@ private fun AmountTextFieldPreview( AmountTextField( value = text, decimals = amount.decimals, - symbol = amount.symbol, - placeholderAlignment = amount.placeholderAlignment, - showPlaceholder = amount.showPlaceholder, onValueChange = { text = it }, textStyle = TangemTheme.typography.h2.copy( color = TangemTheme.colors.text.primary1, @@ -169,28 +141,24 @@ private fun AmountTextFieldPreview( private class AmountTextFieldPreviewProvider : PreviewParameterProvider { override val values = sequenceOf( AmountTextFieldPreviewData( - symbol = "USD", value = "1000000,123123", decimals = 3, placeholderAlignment = TopStart, showPlaceholder = true, ), AmountTextFieldPreviewData( - symbol = null, value = "1000000.123123", decimals = 6, placeholderAlignment = TopStart, showPlaceholder = false, ), AmountTextFieldPreviewData( - symbol = "$", value = null, decimals = 2, showPlaceholder = true, placeholderAlignment = TopCenter, ), AmountTextFieldPreviewData( - symbol = null, value = null, decimals = 2, showPlaceholder = true, @@ -200,7 +168,6 @@ private class AmountTextFieldPreviewProvider : PreviewParameterProvider 0) { + transformedOffset.coerceIn(0, minOf(symbolOffset, formattedText.length)) + } else { + transformedOffset.coerceIn(0, formattedText.length) + } + return transformedOffset } override fun transformedToOriginal(offset: Int): Int { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt index dd7c375e77..28c9082f90 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt @@ -17,10 +17,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import com.tangem.core.ui.components.fields.AmountTextField +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.rememberDecimalFormat /** * Input row for entering amount. Manages correct amount format and validation @@ -76,7 +78,11 @@ fun InputRowEnterAmount( AmountTextField( value = text, decimals = decimals, - symbol = symbol, + visualTransformation = AmountVisualTransformation( + decimals = decimals, + symbol = symbol, + decimalFormat = rememberDecimalFormat(), + ), onValueChange = onValueChange, color = textColor, textStyle = TangemTheme.typography.body2, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt index 40276abe7a..00d4221e37 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt @@ -12,23 +12,28 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.tangem.core.ui.components.fields.AmountTextField +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.rememberDecimalFormat /** * `Input Row Enter Info` for entering amount. Manages correct amount format and validation * @param title title reference * @param text primary text reference + * @param decimals amount text decimal count * @param onValueChange text change callback * @param modifier modifier + * @param symbol amount symbol + * @param info info text * @param titleColor title color * @param textColor text color - * @param isSingleLine text - * @param visualTransformation applied transformation to text + * @param infoColor info text color * @param keyboardOptions keyboard options for field + * @param keyboardActions keyboard actions for field * @param showDivider show divider * * @see [InputRowEnterInfo] @@ -71,7 +76,11 @@ fun InputRowEnterInfoAmount( AmountTextField( value = text, decimals = decimals, - symbol = symbol, + visualTransformation = AmountVisualTransformation( + decimals = decimals, + symbol = symbol, + decimalFormat = rememberDecimalFormat(), + ), onValueChange = onValueChange, color = textColor, textStyle = TangemTheme.typography.body2, diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 020cc01c45..3ca0b57adf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -38,6 +38,30 @@ object BigDecimalFormatter { } } + fun formatCryptoAmountUncapped( + cryptoAmount: BigDecimal?, + cryptoSymbol: String, + decimals: Int, + locale: Locale = Locale.getDefault(), + ): String { + if (cryptoAmount == null) return EMPTY_BALANCE_SIGN + + val formatter = NumberFormat.getNumberInstance(locale).apply { + maximumFractionDigits = decimals + minimumFractionDigits = minOf(2, decimals) + isGroupingUsed = true + roundingMode = RoundingMode.DOWN + } + + return formatter.format(cryptoAmount).let { + if (cryptoSymbol.isEmpty()) { + it + } else { + it + "\u2009$cryptoSymbol" + } + } + } + fun formatCryptoAmount( cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index d16f511bbd..a4306da4f4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -50,6 +50,7 @@ internal sealed class SendStates { val segmentedButtonConfig: PersistentList, val amountTextField: SendTextField.AmountField, val notifications: ImmutableList, + val appCurrencyCode: String, val isFeeLoading: Boolean, ) : SendStates() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt index 9e393e8bae..d12d7d3c28 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt @@ -40,6 +40,7 @@ internal class SendAmountStateConverter( isPrimaryButtonEnabled = false, notifications = persistentListOf(), isFeeLoading = false, + appCurrencyCode = appCurrency.code, segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) { persistentListOf() } else { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt index 950dd0b135..11dfc8f056 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -50,7 +50,7 @@ internal class EthereumCustomFeeConverter( ), SendTextField.CustomFee( value = value.gasPrice.toString(), - decimals = ETHEREUM_GAS_DECIMALS, + decimals = GAS_DECIMALS, symbol = ETHEREUM_GAS_UNIT, title = resourceReference(R.string.send_gas_price), footer = resourceReference(R.string.send_gas_price_footer), @@ -64,7 +64,7 @@ internal class EthereumCustomFeeConverter( SendTextField.CustomFee( value = value.gasLimit.toString(), decimals = GAS_DECIMALS, - symbol = null, + symbol = "", title = resourceReference(R.string.send_gas_limit), footer = resourceReference(R.string.send_gas_limit_footer), onValueChange = { clickIntents.onCustomFeeValueChange(GAS_LIMIT, it) }, @@ -95,57 +95,10 @@ internal class EthereumCustomFeeConverter( ): ImmutableList { val mutableCustomValues = customValues.toMutableList() return mutableCustomValues.apply { - val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals) when (index) { - FEE_AMOUNT -> { - val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals) - val newFeeAmount = newFeeAmountDecimal.movePointRight(this[FEE_AMOUNT].decimals) - val newGasPrice = newFeeAmount.divide(gasLimit, GAS_DECIMALS, RoundingMode.HALF_UP) - set(GAS_PRICE, this[GAS_PRICE].copy(value = newGasPrice.parseBigDecimal(GAS_DECIMALS))) - set( - index, - this[index].copy( - value = value, - label = getFeeFormatted(newFeeAmountDecimal), - ), - ) - } - GAS_PRICE -> { - val newGasPrice = value.parseToBigDecimal(this[GAS_PRICE].decimals) - .movePointLeft(this[GAS_PRICE].decimals) - val newFeeAmount = gasLimit * newGasPrice - set( - FEE_AMOUNT, - this[FEE_AMOUNT].copy( - value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), - label = getFeeFormatted(newFeeAmount), - ), - ) - set(index, this[index].copy(value = value)) - } - else -> { - val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals) - val gasPrice = this[GAS_PRICE].value.parseToBigDecimal(this[GAS_PRICE].decimals) - .movePointLeft(this[FEE_AMOUNT].decimals) - val newFeeAmount = newGasLimit * gasPrice - set( - FEE_AMOUNT, - this[FEE_AMOUNT].copy( - value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), - label = getFeeFormatted(newFeeAmount), - ), - ) - set( - index, - this[index].copy( - value = value, - keyboardOptions = KeyboardOptions( - imeAction = if (!checkExceedBalance(newFeeAmount)) ImeAction.None else ImeAction.Done, - keyboardType = KeyboardType.Number, - ), - ), - ) - } + FEE_AMOUNT -> setOnAmountChange(value, index) + GAS_PRICE -> setOnGasPriceChange(value, index) + else -> setOnGasLimitChange(value, index) } }.toImmutableList() } @@ -170,9 +123,81 @@ internal class EthereumCustomFeeConverter( return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount } + private fun MutableList.setEmpty(index: Int) { + set(index, this[index].copy(value = "")) + } + + private fun MutableList.setOnAmountChange(value: String, index: Int) { + val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals) + if (value.isBlank()) { + setEmpty(FEE_AMOUNT) + setEmpty(GAS_PRICE) + } else { + val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals) + val newFeeAmount = newFeeAmountDecimal.movePointRight(this[FEE_AMOUNT].decimals) + val newGasPrice = newFeeAmount.divide(gasLimit, GAS_DECIMALS, RoundingMode.HALF_UP) + set(GAS_PRICE, this[GAS_PRICE].copy(value = newGasPrice.parseBigDecimal(GAS_DECIMALS))) + set( + index, + this[index].copy( + value = value, + label = getFeeFormatted(newFeeAmountDecimal), + ), + ) + } + } + + private fun MutableList.setOnGasPriceChange(value: String, index: Int) { + val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals) + if (value.isBlank()) { + setEmpty(FEE_AMOUNT) + setEmpty(GAS_PRICE) + } else { + val newGasPrice = value.parseToBigDecimal(this[GAS_PRICE].decimals) + .movePointLeft(this[GAS_PRICE].decimals) + val newFeeAmount = (gasLimit * newGasPrice).movePointLeft(this[FEE_AMOUNT].decimals) + set( + FEE_AMOUNT, + this[FEE_AMOUNT].copy( + value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), + label = getFeeFormatted(newFeeAmount), + ), + ) + set(index, this[index].copy(value = value)) + } + } + + private fun MutableList.setOnGasLimitChange(value: String, index: Int) { + if (value.isBlank()) { + setEmpty(FEE_AMOUNT) + setEmpty(GAS_LIMIT) + } else { + val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals) + val gasPrice = this[GAS_PRICE].value.parseToBigDecimal(this[GAS_PRICE].decimals) + .movePointLeft(this[FEE_AMOUNT].decimals) + val newFeeAmount = newGasLimit * gasPrice + set( + FEE_AMOUNT, + this[FEE_AMOUNT].copy( + value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), + label = getFeeFormatted(newFeeAmount), + ), + ) + set( + index, + this[index].copy( + value = value, + keyboardOptions = KeyboardOptions( + imeAction = if (!checkExceedBalance(newFeeAmount)) ImeAction.None else ImeAction.Done, + keyboardType = KeyboardType.Number, + ), + ), + ) + } + } + companion object { private const val ETHEREUM_GAS_UNIT = "GWEI" - private const val ETHEREUM_GAS_DECIMALS = 18 private const val FEE_AMOUNT = 0 private const val GAS_PRICE = 1 private const val GAS_LIMIT = 2 diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index cd6254be4f..273e457783 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -75,12 +75,16 @@ internal class SendAmountFieldChangeConverter( } private fun SendUiState.emptyState(): SendUiState { + if (amountState == null) return this + val amountTextField = amountState.amountTextField return copy( - amountState = amountState?.copy( + amountState = amountState.copy( isPrimaryButtonEnabled = false, - amountTextField = amountState.amountTextField.copy( + amountTextField = amountTextField.copy( value = "", fiatValue = "", + cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO), + fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO), isError = false, ), ), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt index 43d0d75572..75e5ce97f4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt @@ -24,6 +24,7 @@ internal object AmountStatePreviewData { segmentedButtonConfig = persistentListOf(), notifications = persistentListOf(), isFeeLoading = false, + appCurrencyCode = "usd", amountTextField = SendTextField.AmountField( value = "123.123123123123123123", onValueChange = {}, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt index 4c45622e33..5aa3ec82ac 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -3,42 +3,49 @@ package com.tangem.features.send.impl.presentation.ui.amount import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.BottomCenter -import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection import com.tangem.core.ui.components.fields.AmountTextField +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.core.ui.utils.defaultFormat import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import kotlinx.coroutines.job @Composable -internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolean) { +internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolean, appCurrencyCode: String) { val decimalFormat = rememberDecimalFormat() - val (primaryValue, secondaryValue) = if (sendField.isFiatValue) { - sendField.fiatValue to sendField.value + val isFiatValue = sendField.isFiatValue + val currencyCode = if (isFiatValue) appCurrencyCode else null + val (primaryAmount, primaryValue) = if (isFiatValue) { + sendField.fiatAmount to sendField.fiatValue } else { - sendField.value to sendField.fiatValue + sendField.cryptoAmount to sendField.value } - - val (primaryAmount, secondaryAmount) = if (sendField.isFiatValue) { - sendField.fiatAmount to sendField.cryptoAmount - } else { - sendField.cryptoAmount to sendField.fiatAmount - } - + val requester = remember { FocusRequester() } AmountTextField( value = primaryValue, decimals = primaryAmount.decimals, - symbol = primaryAmount.currencySymbol, + visualTransformation = AmountVisualTransformation( + decimals = primaryAmount.decimals, + symbol = primaryAmount.currencySymbol, + currencyCode = currencyCode, + decimalFormat = decimalFormat, + ), onValueChange = sendField.onValueChange, keyboardOptions = sendField.keyboardOptions, keyboardActions = sendField.keyboardActions, @@ -48,8 +55,8 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea ), isEnabled = isEnabled, isAutoResize = true, - placeholderAlignment = TopCenter, modifier = Modifier + .focusRequester(requester) .padding( top = TangemTheme.dimens.spacing24, start = TangemTheme.dimens.spacing12, @@ -57,7 +64,18 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea ) .requiredHeightIn(min = TangemTheme.dimens.size32), ) + LaunchedEffect(key1 = Unit) { + this.coroutineContext.job.invokeOnCompletion { + requester.requestFocus() + } + } + AmountSecondary(sendField, appCurrencyCode) +} + +@Composable +private fun AmountSecondary(sendField: SendTextField.AmountField, appCurrencyCode: String) { + val secondaryAmount = if (sendField.isFiatValue) sendField.cryptoAmount else sendField.fiatAmount Box( modifier = Modifier .padding( @@ -66,10 +84,18 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea end = TangemTheme.dimens.spacing12, ), ) { - val text = if (sendField.isFiatUnavailable) { - BigDecimalFormatter.EMPTY_BALANCE_SIGN + val text = if (sendField.isFiatValue) { + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = secondaryAmount.value, + cryptoCurrency = secondaryAmount.currencySymbol, + decimals = secondaryAmount.decimals, + ) } else { - "${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}" + BigDecimalFormatter.formatFiatAmount( + fiatAmount = secondaryAmount.value, + fiatCurrencySymbol = secondaryAmount.currencySymbol, + fiatCurrencyCode = appCurrencyCode, + ) } Text( text = text, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt index 5cf940b41b..d9926e3f54 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt @@ -63,6 +63,7 @@ internal fun LazyListScope.amountField( AmountField( sendField = amountState.amountTextField, isEnabled = !amountState.isFeeLoading, + appCurrencyCode = amountState.appCurrencyCode, ) } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt index 26c0380dd5..528cdfea00 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData @@ -29,8 +30,16 @@ internal fun AmountBlock( ) { val amount = amountState.amountTextField - val cryptoAmount = getAmountWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) - val fiatAmount = getAmountWithSymbol(amount.fiatValue, amount.fiatAmount.currencySymbol) + val cryptoAmount = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = amount.cryptoAmount.value, + cryptoCurrency = amount.cryptoAmount.currencySymbol, + decimals = amount.cryptoAmount.decimals, + ) + val fiatAmount = BigDecimalFormatter.formatFiatAmount( + fiatAmount = amount.fiatAmount.value, + fiatCurrencySymbol = amount.fiatAmount.currencySymbol, + fiatCurrencyCode = amountState.appCurrencyCode, + ) val backgroundColor = if (isEditingDisabled) { TangemTheme.colors.button.disabled } else { @@ -77,10 +86,6 @@ internal fun AmountBlock( } } -private fun getAmountWithSymbol(amount: String, symbol: String): String { - return "$amount $symbol" -} - // region Preview @Preview @Composable From e40af8cd6df27da5b41b349a70334ff781d1ca3f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Apr 2024 21:12:37 +0500 Subject: [PATCH 062/109] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index bc227501e8..136cc2857f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -431,7 +431,7 @@ Причина: %1$s\nКод: %2$s Транзакция не выполнена Сумма - Подтвердить + Подтверждение %1$s, %2$s Адрес Код назначения @@ -456,6 +456,7 @@ Максимальная сумма Комиссия не превысит Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение. + Допустим ввод только цифр Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s. Покрытие сетевой комиссии Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса From 738ddb4e813009c3e5ac657ed6ded6d9918ec416 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Apr 2024 11:20:35 +0500 Subject: [PATCH 063/109] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 3 +++ domain/tokens/build.gradle.kts | 1 + .../tokens/GetCryptoCurrencyActionsUseCase.kt | 19 +++++++++++++------ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 7c830de869..54089b880e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -6,6 +6,7 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -191,6 +192,7 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + sendFeatureToggles: SendFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( @@ -199,6 +201,7 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + sendFeatureToggles = sendFeatureToggles, dispatchers = dispatchers, ) } diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index e32512bb66..5d91ab4124 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) + implementation(projects.features.send.api) /** Project - Other */ implementation(projects.core.utils) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index bbbc8c7071..07cdfdabdc 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -10,6 +10,7 @@ import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.isNullOrZero import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -21,12 +22,14 @@ import java.math.BigDecimal * * @property rampManager Ramp manager to check ramp availability */ +@Suppress("LongParameterList") class GetCryptoCurrencyActionsUseCase( private val rampManager: RampStateManager, private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val sendFeatureToggles: SendFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -198,12 +201,16 @@ class GetCryptoCurrencyActionsUseCase( tokenStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?, ): Boolean { - return when (feePaidCurrency) { - FeePaidCurrency.Coin -> !tokenStatus.value.amount.isZero() && coinStatus?.value?.amount.isZero() - FeePaidCurrency.SameCurrency -> tokenStatus.value.amount.isZero() - is FeePaidCurrency.Token -> { - val feePaidTokenBalance = feePaidCurrency.balance - !tokenStatus.value.amount.isZero() && feePaidTokenBalance.isZero() + return if (sendFeatureToggles.isRedesignedSendEnabled) { + tokenStatus.value.amount.isZero() + } else { + when (feePaidCurrency) { + FeePaidCurrency.Coin -> !tokenStatus.value.amount.isZero() && coinStatus?.value?.amount.isZero() + FeePaidCurrency.SameCurrency -> tokenStatus.value.amount.isZero() + is FeePaidCurrency.Token -> { + val feePaidTokenBalance = feePaidCurrency.balance + !tokenStatus.value.amount.isZero() && feePaidTokenBalance.isZero() + } } } } From 5dec166e7caf1a9d9cc28e135c8b3219ccbdf89a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Apr 2024 12:05:24 +0300 Subject: [PATCH 064/109] Updated on 2026-08-14 --- .../tangem/data/visa/DefaultVisaRepository.kt | 38 ++++++++++++------- .../com/tangem/data/visa/utils/VisaConfig.kt | 2 - .../data/visa/utils/VisaCurrencyFactory.kt | 22 +++++------ .../ui/components/visa/VisaDepositButton.kt | 4 +- .../visa/DefaultVisaContractInfoProvider.kt | 33 ++++++++++++---- .../lib/visa/VisaContractInfoProvider.kt | 4 +- ...lancesAndLimits.kt => VisaContractInfo.kt} | 10 ++++- 7 files changed, 74 insertions(+), 39 deletions(-) rename libs/visa/src/main/kotlin/com/tangem/lib/visa/model/{VisaBalancesAndLimits.kt => VisaContractInfo.kt} (83%) diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index 89752a8b76..a817781b53 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -7,6 +7,7 @@ import arrow.fx.coroutines.parZip import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType import com.tangem.common.card.EllipticCurve +import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.visa.utils.VisaConfig @@ -58,7 +59,6 @@ internal class DefaultVisaRepository( override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency { val address = makeAddress(userWalletId) - // val address = "0x40d8194b7168723ece51fa34d16825c60ba03dfa" // for testing fetchVisaCurrencyIfExpired(address, isRefresh) @@ -69,7 +69,7 @@ internal class DefaultVisaRepository( private suspend fun fetchVisaCurrencyIfExpired(address: String, isRefresh: Boolean) { cacheRegistry.invokeOnExpire( - key = getBalancesAndLimitsKey(address), + key = getVisaCurrencyKey(address), skipCache = isRefresh, block = { fetchVisaCurrency(address) }, ) @@ -78,12 +78,12 @@ internal class DefaultVisaRepository( private suspend fun fetchVisaCurrency(address: String) { parZip( dispatchers.io, - { visaContractInfoProvider.getBalancesAndLimits(address) }, + { visaContractInfoProvider.getContractInfo(address) }, { getFiatRate() }, - { balancesAndLimits, fiatRate -> + { contractInfo, fiatRate -> fetchedCurrencies.update { value -> value.apply { - put(address, currencyFactory.create(balancesAndLimits, fiatRate)) + put(address, currencyFactory.create(contractInfo, fiatRate)) } } }, @@ -96,8 +96,7 @@ internal class DefaultVisaRepository( isRefresh: Boolean, ): Flow> { val userWallet = findVisaUserWallet(userWalletId) - val cardPubKey = getCardPubKey(userWallet).toHexString() - // val cardPubKey = "02C2BBA0DA1E066EA968C1EB129499F6DEBC5FD82D70D61DCAF691CDB69AF5D8B9" // for testing + val cardPubKey = getCardPubKey(userWallet) val pager = Pager( config = PagingConfig( pageSize = pageSize, @@ -124,8 +123,7 @@ internal class DefaultVisaRepository( override suspend fun getTxDetails(userWalletId: UserWalletId, txId: String): VisaTxDetails { return withContext(dispatchers.io) { val userWallet = findVisaUserWallet(userWalletId) - val cardPubKey = getCardPubKey(userWallet).toHexString() - // val cardPubKey = "02C2BBA0DA1E066EA968C1EB129499F6DEBC5FD82D70D61DCAF691CDB69AF5D8B9" // for testing + val cardPubKey = getCardPubKey(userWallet) val transaction = fetchedHistoryItems.value[cardPubKey]?.firstOrNull { it.transactionId.toString() == txId } @@ -139,6 +137,8 @@ internal class DefaultVisaRepository( } private suspend fun makeAddress(userWalletId: UserWalletId): String { + if (IS_DEMO_MODE_ENABLED) return DEMO_ADDRESS + val userWallet = findVisaUserWallet(userWalletId) val walletAddresses = makeWalletAddresses(userWallet) val walletAddress = walletAddresses.firstOrNull { it.type == AddressType.Default } @@ -161,16 +161,18 @@ internal class DefaultVisaRepository( private fun makeWalletAddresses(userWallet: UserWallet): Set
{ val walletBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() - return walletBlockchain.makeAddresses(getCardPubKey(userWallet)) + return walletBlockchain.makeAddresses(getCardPubKey(userWallet).hexToBytes()) } - private fun getCardPubKey(userWallet: UserWallet): ByteArray { + private fun getCardPubKey(userWallet: UserWallet): String { + if (IS_DEMO_MODE_ENABLED) return DEMO_PUBLIC_KEY + val cardWallet = userWallet.scanResponse.card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } requireNotNull(cardWallet) { "Secp256k1 card wallet not found" } - return cardWallet.publicKey + return cardWallet.publicKey.toHexString() } private suspend fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet { @@ -184,7 +186,15 @@ internal class DefaultVisaRepository( return userWallet } - private fun getBalancesAndLimitsKey(address: String): String { - return "visa_balances_and_limits_$address" + private fun getVisaCurrencyKey(address: String): String { + return "visa_currency_$address" + } + + private companion object { + // Must be `false` in production + const val IS_DEMO_MODE_ENABLED = false + + const val DEMO_ADDRESS = "0x40d8194b7168723ece51fa34d16825c60ba03dfa" + const val DEMO_PUBLIC_KEY = "02C2BBA0DA1E066EA968C1EB129499F6DEBC5FD82D70D61DCAF691CDB69AF5D8B9" } } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt index e450a769cc..6b3e3f3f8b 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt @@ -6,9 +6,7 @@ internal object VisaConfig { const val NETWORK_NAME = "Polygon PoS" - const val TOKEN_SYMBOL = "USDT" const val TOKEN_ID = "tether" - const val TOKEN_DECIMALS = 8 val fiatCurrency = AppCurrency( code = "EUR", diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt index 8f3e95d73a..3c9f55e37f 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt @@ -1,7 +1,7 @@ package com.tangem.data.visa.utils import com.tangem.domain.visa.model.VisaCurrency -import com.tangem.lib.visa.model.VisaBalancesAndLimits +import com.tangem.lib.visa.model.VisaContractInfo import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.joda.time.Instant @@ -10,22 +10,22 @@ import java.math.BigInteger internal class VisaCurrencyFactory { - fun create(balancesAndLimits: VisaBalancesAndLimits, fiatRate: BigDecimal?): VisaCurrency { + fun create(contractInfo: VisaContractInfo, fiatRate: BigDecimal?): VisaCurrency { val now = Instant.now() - val currentLimit = if (balancesAndLimits.limitsChangeDate > now) { - balancesAndLimits.oldLimits + val currentLimit = if (contractInfo.limitsChangeDate > now) { + contractInfo.oldLimits } else { - balancesAndLimits.newLimits + contractInfo.newLimits } val remainingOtpLimit = getRemainingOtp(currentLimit, now) return VisaCurrency( - symbol = VisaConfig.TOKEN_SYMBOL, + symbol = contractInfo.token.symbol, networkName = VisaConfig.NETWORK_NAME, - decimals = VisaConfig.TOKEN_DECIMALS, + decimals = contractInfo.token.decimals, fiatRate = fiatRate, fiatCurrency = VisaConfig.fiatCurrency, - balances = with(balancesAndLimits) { + balances = with(contractInfo) { VisaCurrency.Balances( total = balances.total, verified = balances.verified, @@ -44,7 +44,7 @@ internal class VisaCurrencyFactory { ) } - private fun getRemainingOtp(currentLimit: VisaBalancesAndLimits.Limits, now: Instant): BigDecimal { + private fun getRemainingOtp(currentLimit: VisaContractInfo.Limits, now: Instant): BigDecimal { if (currentLimit.expirationDate >= now) { return currentLimit.spendLimit.limit - currentLimit.spendLimit.spent } @@ -52,7 +52,7 @@ internal class VisaCurrencyFactory { return currentLimit.spendLimit.limit } - private fun getRemainingNoOtp(currentLimit: VisaBalancesAndLimits.Limits, now: Instant): BigDecimal { + private fun getRemainingNoOtp(currentLimit: VisaContractInfo.Limits, now: Instant): BigDecimal { if (currentLimit.expirationDate >= now) { return currentLimit.noOtpLimit.limit - currentLimit.noOtpLimit.spent } @@ -60,7 +60,7 @@ internal class VisaCurrencyFactory { return currentLimit.noOtpLimit.limit } - private fun getLimitsExpirationDate(currentLimits: VisaBalancesAndLimits.Limits, now: Instant): DateTime { + private fun getLimitsExpirationDate(currentLimits: VisaContractInfo.Limits, now: Instant): DateTime { val expirationDate = if (currentLimits.expirationDate >= now) { currentLimits.expirationDate.toDateTime() } else { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaDepositButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaDepositButton.kt index 30bb253d24..395e9499a4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaDepositButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaDepositButton.kt @@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.visa import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.components.PrimaryButtonIconStart import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.DepositButtonState @@ -10,7 +10,7 @@ private const val DEPOSIT_BUTTON_CONTENT_TYPE = "DepositButton" internal fun LazyListScope.depositButton(state: DepositButtonState, modifier: Modifier = Modifier) { item(key = DEPOSIT_BUTTON_CONTENT_TYPE, contentType = DEPOSIT_BUTTON_CONTENT_TYPE) { - SecondaryButtonIconStart( + PrimaryButtonIconStart( modifier = modifier, text = "Deposit", iconResId = R.drawable.ic_arrow_down_24, diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt index 21989a9ee6..312f708435 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt @@ -1,9 +1,8 @@ package com.tangem.lib.visa import arrow.fx.coroutines.parZip -import com.tangem.lib.visa.model.VisaBalancesAndLimits -import com.tangem.lib.visa.model.VisaBalancesAndLimits.Balances -import com.tangem.lib.visa.model.VisaBalancesAndLimits.Limits +import com.tangem.lib.visa.model.VisaContractInfo +import com.tangem.lib.visa.model.VisaContractInfo.* import com.tangem.lib.visa.utils.toBigDecimal import com.tangem.lib.visa.utils.toInstant import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -21,7 +20,7 @@ internal class DefaultVisaContractInfoProvider( private val dispatchers: CoroutineDispatcherProvider, ) : VisaContractInfoProvider { - override suspend fun getBalancesAndLimits(walletAddress: String): VisaBalancesAndLimits { + override suspend fun getContractInfo(walletAddress: String): VisaContractInfo { return parZip( dispatchers.io, { loadPaymentAccount(walletAddress) }, @@ -64,15 +63,35 @@ internal class DefaultVisaContractInfoProvider( private suspend fun fetchBalancesAndLimits( paymentAccount: TangemPaymentAccount, paymentToken: PaymentTokenInfo, - ): VisaBalancesAndLimits = parZip( + ): VisaContractInfo = parZip( dispatchers.io, + { fetchToken(paymentAccount) }, { fetchBalances(paymentAccount, paymentToken) }, { fetchLimits(paymentAccount, paymentToken) }, - { balances, (oldLimit, newLimit, changeDate) -> - VisaBalancesAndLimits(balances, oldLimit, newLimit, changeDate) + { token, balances, (oldLimit, newLimit, changeDate) -> + VisaContractInfo(token, balances, oldLimit, newLimit, changeDate) }, ) + private suspend fun fetchToken(paymentAccount: TangemPaymentAccount): Token { + val paymentTokenContractAddress = paymentAccount.paymentToken().send() + val paymentTokenContract = ERC20.load(paymentTokenContractAddress, web3j, transactionManager, gasProvider) + + return parZip( + dispatchers.io, + { paymentTokenContract.name().send() }, + { paymentTokenContract.symbol().send() }, + { paymentTokenContract.decimals().send() }, + ) { name, symbol, decimals -> + Token( + name = name, + symbol = symbol, + decimals = decimals.toInt(), + address = paymentTokenContractAddress, + ) + } + } + private suspend fun fetchBalances(paymentAccount: TangemPaymentAccount, paymentToken: PaymentTokenInfo): Balances { return parZip( dispatchers.io, diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt index 7829c34135..7ae3b7e0dc 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt @@ -2,7 +2,7 @@ package com.tangem.lib.visa import com.ihsanbal.logging.Level import com.ihsanbal.logging.LoggingInterceptor -import com.tangem.lib.visa.model.VisaBalancesAndLimits +import com.tangem.lib.visa.model.VisaContractInfo import com.tangem.lib.visa.utils.VisaConfig import com.tangem.lib.visa.utils.VisaConfig.NETWORK_LOGS_TAG import com.tangem.lib.visa.utils.toHexString @@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit interface VisaContractInfoProvider { - suspend fun getBalancesAndLimits(walletAddress: String): VisaBalancesAndLimits + suspend fun getContractInfo(walletAddress: String): VisaContractInfo class Builder( private val isNetworkLoggingEnabled: Boolean, diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaBalancesAndLimits.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaContractInfo.kt similarity index 83% rename from libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaBalancesAndLimits.kt rename to libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaContractInfo.kt index 50145e05da..e2e6f98d58 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaBalancesAndLimits.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaContractInfo.kt @@ -4,13 +4,21 @@ import org.joda.time.Instant import java.math.BigDecimal import java.math.BigInteger -data class VisaBalancesAndLimits( +data class VisaContractInfo( + val token: Token, val balances: Balances, val oldLimits: Limits, val newLimits: Limits, val limitsChangeDate: Instant, ) { + data class Token( + val name: String, + val symbol: String, + val decimals: Int, + val address: String, + ) + data class Balances( val total: BigDecimal, val verified: BigDecimal, From 8fa38fe87a4b224b674446ef0d016b51227d2643 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Apr 2024 15:24:56 +0100 Subject: [PATCH 065/109] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/common/DialogManager.kt | 5 +++++ .../details/redux/walletconnect/WalletConnectMiddleware.kt | 7 +++++++ .../details/redux/walletconnect/WalletConnectState.kt | 7 ++++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 68fc7580bc..cbbcee8526 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -112,6 +112,11 @@ class DialogManager : StoreSubscriber { context = context, ) } + is WalletConnectDialog.UnsupportedDapp -> SimpleAlertDialog.create( + titleRes = R.string.wallet_connect_title, + messageRes = R.string.wallet_connect_error_unsupported_dapp, + context = context, + ) is WalletConnectDialog.SessionProposalDialog -> { SessionProposalDialog.create( sessionProposal = state.dialog.sessionProposal, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 47c26d8452..4a26922744 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -352,6 +352,13 @@ class WalletConnectMiddleware { ), ) } + is WalletConnectError.UnsupportedDApp -> { + store.dispatchOnMain( + GlobalAction.ShowDialog( + WalletConnectDialog.UnsupportedDapp, + ), + ) + } is WalletConnectError.ExternalApprovalError -> { Timber.e(action.error, "ExternalApprovalError ${action.error.message}") // do not show dialog on this event diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index c696a29485..68cfd4255b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -88,11 +88,12 @@ data class WalletForSession( sealed class WalletConnectDialog : StateDialog { data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog() - object UnsupportedCard : WalletConnectDialog() + data object UnsupportedCard : WalletConnectDialog() data class UnsupportedNetwork(val networks: List? = null) : WalletConnectDialog() + data object UnsupportedDapp : WalletConnectDialog() data class AddNetwork(val networks: List) : WalletConnectDialog() - object OpeningSessionRejected : WalletConnectDialog() - object SessionTimeout : WalletConnectDialog() + data object OpeningSessionRejected : WalletConnectDialog() + data object SessionTimeout : WalletConnectDialog() data class ApproveWcSession( val session: WalletConnectSession, val networks: List, From bc001987977a5d0ef942117f1f5d50295cce51b6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Apr 2024 17:38:36 +0100 Subject: [PATCH 066/109] Updated on 2026-08-14 --- .../tap/common/extensions/WalletManager.kt | 6 ++--- .../note/redux/OnboardingNoteMiddleware.kt | 9 ++++++- .../twins/redux/TwinCardsMiddleware.kt | 10 +++++++- .../middlewares/TradeCryptoMiddleware.kt | 6 ++--- .../exchangeServices/BuyExchangeService.kt | 8 +++--- .../CurrencyExchangeManager.kt | 9 +++---- .../exchangeServices/ExchangeService.kt | 8 +++--- .../mercuryo/MercuryoService.kt | 9 ++++--- .../moonpay/MoonPayService.kt | 25 +++++++++++++++---- 9 files changed, 57 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 11c26259ee..f4df731c74 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.AddressType import com.tangem.common.services.Result import com.tangem.domain.common.extensions.amountToCreateAccount +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.TestActions import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.domain.TapError @@ -58,14 +59,13 @@ suspend fun WalletManager.safeUpdate(isDemoCard: Boolean): Result = try } } -internal fun WalletManager.getTopUpUrl(): String? { +internal fun WalletManager.getTopUpUrl(cryptoCurrency: CryptoCurrency): String? { val globalState = store.state.globalState val defaultAddress = wallet.address return globalState.exchangeManager.getUrl( action = CurrencyExchangeManager.Action.Buy, - blockchain = wallet.blockchain, - cryptoCurrencyName = wallet.blockchain.currency, + cryptoCurrency = cryptoCurrency, fiatCurrencyName = globalState.appCurrency.code, walletAddress = defaultAddress, isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 114dedc7df..ca78c542d3 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -4,8 +4,10 @@ import com.tangem.common.CompletionResult import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction +import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.entities.ProgressState @@ -171,8 +173,13 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch return } - val topUpUrl = walletManager.getTopUpUrl() ?: return val blockchain = walletManager.wallet.blockchain + val cryptoCurrency = CryptoCurrencyFactory().createCoin( + blockchain, + null, + scanResponse.derivationStyleProvider, + ) ?: return + val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain) Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType)) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index c2fc60baaa..e973dae678 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -6,8 +6,10 @@ import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletIdBuilder @@ -293,8 +295,14 @@ private fun handle(action: Action, dispatch: DispatchFunction) { return } - val topUpUrl = walletManager.getTopUpUrl() ?: return + val scanResponse = onboardingManager?.scanResponse ?: return val blockchain = walletManager.wallet.blockchain + val cryptoCurrency = CryptoCurrencyFactory().createCoin( + blockchain, + null, + scanResponse.derivationStyleProvider, + ) ?: return + val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain) Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType)) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index a76ae2c37e..aa15f116e8 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -87,8 +87,7 @@ object TradeCryptoMiddleware { val exchangeManager = store.state.globalState.exchangeManager val topUrl = exchangeManager.getUrl( action = CurrencyExchangeManager.Action.Buy, - blockchain = blockchain, - cryptoCurrencyName = currency.symbol, + cryptoCurrency = currency, fiatCurrencyName = action.appCurrencyCode, walletAddress = networkAddress, isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, @@ -140,8 +139,7 @@ object TradeCryptoMiddleware { store.state.globalState.exchangeManager.getUrl( action = CurrencyExchangeManager.Action.Sell, - blockchain = Blockchain.fromId(currency.network.id.value), - cryptoCurrencyName = currency.symbol, + cryptoCurrency = currency, fiatCurrencyName = action.appCurrencyCode, walletAddress = networkAddress, isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt index 5f48db2717..e6c523e666 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt @@ -1,6 +1,6 @@ package com.tangem.tap.network.exchangeServices -import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService import com.tangem.tap.scope @@ -38,16 +38,14 @@ internal class BuyExchangeService( override fun getUrl( action: CurrencyExchangeManager.Action, - blockchain: Blockchain, - cryptoCurrencyName: String, + cryptoCurrency: CryptoCurrency, fiatCurrencyName: String, walletAddress: String, isDarkTheme: Boolean, ): String? { return currentService.getUrl( action, - blockchain, - cryptoCurrencyName, + cryptoCurrency, fiatCurrencyName, walletAddress, isDarkTheme, diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index 1f0dc4ea68..e962f7ebf4 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -8,9 +8,9 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.safeUpdate -import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner import com.tangem.tap.domain.model.Currency @@ -48,19 +48,18 @@ class CurrencyExchangeManager( override fun getUrl( action: Action, - blockchain: Blockchain, - cryptoCurrencyName: CryptoCurrencyName, + cryptoCurrency: CryptoCurrency, fiatCurrencyName: String, walletAddress: String, isDarkTheme: Boolean, ): String? { + val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl() val urlBuilder = getExchangeUrlBuilder(action) return urlBuilder.getUrl( action, - blockchain, - cryptoCurrencyName, + cryptoCurrency, fiatCurrencyName, walletAddress, isDarkTheme, diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt index c3f3587c26..e9b17488e3 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt @@ -1,6 +1,6 @@ package com.tangem.tap.network.exchangeServices -import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.feature.Feature import com.tangem.tap.domain.model.Currency @@ -24,8 +24,7 @@ interface ExchangeService : Feature, Exchanger, ExchangeUrlBuilder { override fun availableForSell(currency: Currency): Boolean = false override fun getUrl( action: CurrencyExchangeManager.Action, - blockchain: Blockchain, - cryptoCurrencyName: String, + cryptoCurrency: CryptoCurrency, fiatCurrencyName: String, walletAddress: String, isDarkTheme: Boolean, @@ -56,8 +55,7 @@ interface ExchangeUrlBuilder { @Suppress("LongParameterList") fun getUrl( action: CurrencyExchangeManager.Action, - blockchain: Blockchain, - cryptoCurrencyName: String, + cryptoCurrency: CryptoCurrency, fiatCurrencyName: String, walletAddress: String, isDarkTheme: Boolean, diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index be796e98e7..d653e2babe 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -6,7 +6,7 @@ import com.tangem.common.extensions.calculateSha512 import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.common.services.performRequest -import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService @@ -57,20 +57,21 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E override fun getUrl( action: CurrencyExchangeManager.Action, - blockchain: Blockchain, - cryptoCurrencyName: CryptoCurrencyName, + cryptoCurrency: CryptoCurrency, fiatCurrencyName: String, walletAddress: String, isDarkTheme: Boolean, ): String { if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException() + val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) + val builder = Uri.Builder() .scheme(ExchangeUrlBuilder.SCHEME) .authority("exchange.mercuryo.io") .appendQueryParameter("widget_id", environment.widgetId) .appendQueryParameter("type", action.name.lowercase()) - .appendQueryParameter("currency", cryptoCurrencyName) + .appendQueryParameter("currency", cryptoCurrency.symbol) .appendQueryParameter("address", walletAddress) .appendQueryParameter("signature", signature(walletAddress)) .appendQueryParameter("fix_currency", "true") diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 3e5a70b2a2..228273c9bd 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -7,7 +7,7 @@ import com.tangem.common.services.Result import com.tangem.common.services.performRequest import com.tangem.datasource.api.common.createRetrofitInstance import com.tangem.domain.common.extensions.withIOContext -import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService @@ -51,6 +51,7 @@ class MoonPayService( checkGeneralRequirements(currency) && checkUSARequirements(userStatus, currency) } .mapNotNull { currency -> + if (!currency.isSellSupported) return@mapNotNull null MoonPayAvailableCurrency( currencyCode = currency.code, networkCode = currency.metadata?.networkCode ?: return@mapNotNull null, @@ -106,19 +107,33 @@ class MoonPayService( override fun getUrl( action: CurrencyExchangeManager.Action, - blockchain: Blockchain, - cryptoCurrencyName: CryptoCurrencyName, + cryptoCurrency: CryptoCurrency, fiatCurrencyName: String, walletAddress: String, isDarkTheme: Boolean, - ): String { + ): String? { if (action == CurrencyExchangeManager.Action.Buy) throw UnsupportedOperationException() + val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) + val supportedCurrency = blockchain.moonPaySupportedCurrency ?: return null + val moonpayCurrency = status?.availableForSell?.firstOrNull { + when (cryptoCurrency) { + is CryptoCurrency.Coin -> { + it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) && + it.currencyCode.equals(other = supportedCurrency.currencyCode, ignoreCase = true) + } + is CryptoCurrency.Token -> { + it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) && + it.contractAddress.equals(other = cryptoCurrency.contractAddress, ignoreCase = true) + } + } + } ?: return null + val uri = Uri.Builder() .scheme(SCHEME) .authority(URL_SELL) .appendQueryParameter("apiKey", apiKey) - .appendQueryParameter("baseCurrencyCode", cryptoCurrencyName) + .appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase()) .appendQueryParameter("refundWalletAddress", walletAddress) .appendQueryParameter("redirectURL", "tangem://redirect_sell") From 788a4718710fa7456a40b44eff73b8473abfb662 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Apr 2024 11:07:52 +0100 Subject: [PATCH 067/109] Updated on 2026-08-14 --- gradle/dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 42119bfeb6..8731df13aa 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,9 +85,9 @@ leakcanary = "2.13" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-547" +tangemBlockchainSdk = "release-app_5.9-560" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-339" +tangemCardSdk = "release-app_5.9-340" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem From 7e1e05e72a8fca916cafb6b6319ee76b2565a485 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Apr 2024 20:32:44 +0500 Subject: [PATCH 068/109] Updated on 2026-08-14 --- .../presentation/viewmodel/SendViewModel.kt | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 67e152dae9..54b90feac1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -32,7 +32,6 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -75,15 +74,15 @@ internal class SendViewModel @Inject constructor( private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, - private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, + private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, + private val getCryptoCurrencyStatusesSyncUseCase: GetCryptoCurrencyStatusesSyncUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val getFeeUseCase: GetFeeUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, - private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @@ -403,31 +402,38 @@ internal class SendViewModel @Inject constructor( .filterNot { it.walletId == userWalletId || it.isLocked } .map { wallet -> async(dispatchers.io) { - getCryptoCurrenciesUseCase.getSync(wallet.walletId) - .fold( - ifRight = { currencyItem -> - val walletCurrency = currencyItem.firstOrNull { - it.network.id == cryptoCurrency.network.id - } ?: return@fold null - val addresses = walletManagersFacade.getAddress( - userWalletId = wallet.walletId, - network = walletCurrency.network, - ) - return@fold addresses.firstOrNull()?.let { - AvailableWallet( - name = wallet.name, - address = it.value, - ) - } - }, - ifLeft = { null }, - ) + wallet.toAvailableWallet() } } }.awaitAll() } } + private suspend fun UserWallet.toAvailableWallet(): AvailableWallet? { + return if (!isMultiCurrency) { + val status = getCryptoCurrencyStatusSyncUseCase(walletId).getOrNull() + val address = status?.value?.networkAddress + address?.let { + AvailableWallet( + name = name, + address = it.defaultAddress.value, + ) + } + } else { + val statuses = getCryptoCurrencyStatusesSyncUseCase(walletId).getOrNull() + val walletCurrency = statuses?.firstOrNull { + it.currency.network.id == cryptoCurrency.network.id + } + val address = walletCurrency?.value?.networkAddress + address?.let { + AvailableWallet( + name = name, + address = it.defaultAddress.value, + ) + } + } + } + private fun getTxHistory(): Flow> { return getFixedTxHistoryItemsUseCase( userWalletId = userWalletId, From 28b202b48dea4265db272a111d913ee2b402df3c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Apr 2024 13:36:35 +0500 Subject: [PATCH 069/109] Updated on 2026-08-14 --- .../repository/DefaultTxHistoryRepository.kt | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index 9f4801ff8c..dba0ed594a 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -21,6 +21,7 @@ import com.tangem.domain.walletmanager.utils.SdkPageConverter import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +import timber.log.Timber class DefaultTxHistoryRepository( private val cacheRegistry: CacheRegistry, @@ -82,16 +83,21 @@ class DefaultTxHistoryRepository( pageSize: Int, refresh: Boolean, ): List { - cacheRegistry.invokeOnExpire( - key = getTxHistoryPageKey(currency, userWalletId, Page.Initial), - skipCache = refresh, - block = { fetchFixedSizeTxHistoryItems(userWalletId, currency, pageSize) }, - ) - val txs = txHistoryItemsStore.getSyncOrNull( - key = TxHistoryItemsStore.Key(userWalletId, currency), - page = Page.Initial, - )?.items - return txs ?: emptyList() + return try { + cacheRegistry.invokeOnExpire( + key = getTxHistoryPageKey(currency, userWalletId, Page.Initial), + skipCache = refresh, + block = { fetchFixedSizeTxHistoryItems(userWalletId, currency, pageSize) }, + ) + val txs = txHistoryItemsStore.getSyncOrNull( + key = TxHistoryItemsStore.Key(userWalletId, currency), + page = Page.Initial, + )?.items + txs ?: emptyList() + } catch (e: Throwable) { + Timber.e(e, "Unable to load the transaction history for the requested page: ${Page.Initial}") + emptyList() + } } private fun getTxHistoryPageKey(currency: CryptoCurrency, userWalletId: UserWalletId, page: Page): String { From ae432b0fd1c11dbd06259ec8d129d8a942ad45ec Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Apr 2024 15:11:44 +0500 Subject: [PATCH 070/109] Updated on 2026-08-14 --- .../send/impl/presentation/ui/SendScreen.kt | 1 + .../ui/recipient/SendRecipientContent.kt | 124 ++++++++++++------ 2 files changed, 83 insertions(+), 42 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index b227fe07fe..fe28791ec9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -122,6 +122,7 @@ private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentS SendUiStateType.Recipient -> SendRecipientContent( uiState = uiState.recipientState, clickIntents = uiState.clickIntents, + isBalanceHidden = uiState.isBalanceHidden, ) SendUiStateType.Fee -> SendSpeedAndFeeContent( state = uiState.feeState, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index 93910bc2a6..539d82e49c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.inputrow.InputRowRecipient import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -25,6 +26,7 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.ui.common.FooterContainer import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.ImmutableList @@ -33,7 +35,11 @@ private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" private const val MEMO_FIELD_KEY = "MEMO_FIELD_KEY" @Composable -internal fun SendRecipientContent(uiState: SendStates.RecipientState?, clickIntents: SendClickIntents) { +internal fun SendRecipientContent( + uiState: SendStates.RecipientState?, + clickIntents: SendClickIntents, + isBalanceHidden: Boolean, +) { if (uiState == null) return val recipients = uiState.recent val wallets = uiState.wallets @@ -47,56 +53,89 @@ internal fun SendRecipientContent(uiState: SendStates.RecipientState?, clickInte .background(TangemTheme.colors.background.tertiary) .padding(horizontal = TangemTheme.dimens.spacing16), ) { - item(key = ADDRESS_FIELD_KEY) { - FooterContainer( - footer = stringResource(R.string.send_recipient_address_footer, uiState.network), - ) { - InputRowRecipient( - value = address.value, - title = address.label, - placeholder = address.placeholder, - onValueChange = address.onValueChange, - onPasteClick = { clickIntents.onRecipientAddressValueChange(it, EnterAddressSource.PasteButton) }, - isError = isError, - isLoading = isValidating, - error = address.error, - modifier = Modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) - } - } - if (memoField != null) { - item(key = MEMO_FIELD_KEY) { - val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText - TextFieldWithPaste( - value = memoField.value, - label = memoField.label, - placeholder = placeholder, - footer = stringResource(R.string.send_recipient_memo_footer), - onValueChange = memoField.onValueChange, - onPasteClick = clickIntents::onRecipientMemoValueChange, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing20), - isError = memoField.isError, - error = memoField.error, - isReadOnly = !memoField.isEnabled, - ) - } - } + addressItem( + address = address, + network = uiState.network, + isError = isError, + isValidating = isValidating, + onAddressChange = clickIntents::onRecipientAddressValueChange, + ) + memoField( + memoField = memoField, + onMemoChange = clickIntents::onRecipientMemoValueChange, + ) listHeaderItem( titleRes = R.string.send_recipient_wallets_title, isVisible = wallets.isNotEmpty() && wallets.first().isVisible, isFirst = true, ) - listItem(wallets, clickIntents, isLast = recipients.isEmpty()) + listItem( + list = wallets, + clickIntents = clickIntents, + isLast = recipients.isEmpty(), + isBalanceHidden = isBalanceHidden, + ) listHeaderItem( titleRes = R.string.send_recent_transactions, isVisible = recipients.isNotEmpty() && recipients.first().isVisible, isFirst = wallets.isEmpty(), ) - listItem(recipients, clickIntents, isLast = true) + listItem( + list = recipients, + clickIntents = clickIntents, + isLast = true, + isBalanceHidden = isBalanceHidden, + ) + } +} + +private fun LazyListScope.addressItem( + address: SendTextField.RecipientAddress, + network: String, + isError: Boolean, + isValidating: Boolean, + onAddressChange: (String, EnterAddressSource?) -> Unit, +) { + item(key = ADDRESS_FIELD_KEY) { + FooterContainer( + footer = stringResource(R.string.send_recipient_address_footer, network), + ) { + InputRowRecipient( + value = address.value, + title = address.label, + placeholder = address.placeholder, + onValueChange = address.onValueChange, + onPasteClick = { onAddressChange(it, EnterAddressSource.PasteButton) }, + isError = isError, + isLoading = isValidating, + error = address.error, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } + } +} + +private fun LazyListScope.memoField(memoField: SendTextField.RecipientMemo?, onMemoChange: (String) -> Unit) { + if (memoField != null) { + item(key = MEMO_FIELD_KEY) { + val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText + TextFieldWithPaste( + value = memoField.value, + label = memoField.label, + placeholder = placeholder, + footer = stringResource(R.string.send_recipient_memo_footer), + onValueChange = memoField.onValueChange, + onPasteClick = onMemoChange, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing20), + isError = memoField.isError, + error = memoField.error, + isReadOnly = !memoField.isEnabled, + ) + } } } @@ -154,6 +193,7 @@ private fun LazyListScope.listItem( list: ImmutableList, clickIntents: SendClickIntents, isLast: Boolean, + isBalanceHidden: Boolean, ) { items( count = list.size, @@ -173,7 +213,7 @@ private fun LazyListScope.listItem( ) { ListItemWithIcon( title = title, - subtitle = item.subtitle.resolveReference(), + subtitle = if (isBalanceHidden) STARS else item.subtitle.resolveReference(), info = item.timestamp?.resolveReference(), subtitleEndOffset = item.subtitleEndOffset, subtitleIconRes = item.subtitleIconRes, From 046b64701a79f741808ecafb29d3789ac8559042 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Apr 2024 15:14:22 +0500 Subject: [PATCH 071/109] Updated on 2026-08-14 --- .../presentation/QrScanningCameraDeniedBottomSheet.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt index 6edcbe2ea2..31f79da70e 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt @@ -33,7 +33,7 @@ private fun CameraDeniedBottomSheet(content: CameraDeniedBottomSheetConfig) { title = stringResource(id = R.string.qr_scanner_camera_denied_settings_button), icon = R.drawable.ic_settings_24, onItemsClick = { - val intent: Intent = Intent( + val intent = Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", context.packageName, null), ) @@ -47,7 +47,7 @@ private fun CameraDeniedBottomSheet(content: CameraDeniedBottomSheetConfig) { ) SimpleSettingsRow( title = stringResource(id = R.string.common_close), - icon = R.drawable.ic_close, + icon = R.drawable.ic_close_24, onItemsClick = content.onCancelClick, modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), ) From ed3ee24cec3706846288aac1d29090ff02197132 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Apr 2024 15:15:18 +0500 Subject: [PATCH 072/109] Updated on 2026-08-14 --- .../send/impl/presentation/viewmodel/SendViewModel.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 67e152dae9..010b405e81 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -618,7 +618,9 @@ internal class SendViewModel @Inject constructor( network = cryptoCurrency.network, address = value, ).getOrElse { false } - onEnteredValidAddress(isValidAddress) + val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses + ?.any { it.value == value } ?: true + onEnteredValidAddress(isValidAddress, isAddressInWallet) return isValidAddress } @@ -632,12 +634,13 @@ internal class SendViewModel @Inject constructor( } ?: false } - private fun onEnteredValidAddress(isValidAddress: Boolean) { + private fun onEnteredValidAddress(isValidAddress: Boolean, isAddressInWallet: Boolean) { val recipientState = uiState.recipientState ?: return + val isVisible = isAddressInWallet || !isValidAddress uiState = uiState.copy( recipientState = recipientState.copy( - recent = recipientState.recent.map { it.copy(isVisible = !isValidAddress) }.toPersistentList(), - wallets = recipientState.wallets.map { it.copy(isVisible = !isValidAddress) }.toPersistentList(), + recent = recipientState.recent.map { it.copy(isVisible = isVisible) }.toPersistentList(), + wallets = recipientState.wallets.map { it.copy(isVisible = isVisible) }.toPersistentList(), ), ) } From 4bd283457bdbd980c96a54d601b9e110f1cbccd1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Apr 2024 15:15:39 +0500 Subject: [PATCH 073/109] Updated on 2026-08-14 --- .../ui/recipient/TextFieldWithPaste.kt | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt index 6109c5ff50..5c0ca33a84 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -1,5 +1,8 @@ package com.tangem.features.send.impl.presentation.ui.recipient +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Text @@ -63,17 +66,29 @@ internal fun TextFieldWithPaste( .padding(top = TangemTheme.dimens.spacing6), ) } - CrossIcon( - onClick = onPasteClick, + AnimatedVisibility( + visible = !isReadOnly, + label = "Animate read only status change", + enter = fadeIn(), + exit = fadeOut(), modifier = Modifier .align(CenterVertically), - ) + ) { + CrossIcon( + onClick = onPasteClick, + ) + } } - if (!isReadOnly) { + AnimatedVisibility( + visible = !isReadOnly, + label = "Animate read only status change", + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier.align(CenterEnd), + ) { PasteButton( isPasteButtonVisible = value.isBlank(), onClick = onPasteClick, - modifier = Modifier.align(CenterEnd), ) } } From 0b32fcb92580bee99ea7d657927e524246ddd7a7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Apr 2024 11:30:31 +0500 Subject: [PATCH 074/109] Updated on 2026-08-14 --- .../send/impl/presentation/state/SendUiState.kt | 1 + .../presentation/state/fee/FeeStateFactory.kt | 16 +++++++++++++--- .../state/fee/SendFeeStateConverter.kt | 1 + .../state/previewdata/FeeStatePreviewData.kt | 1 + 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index a4306da4f4..c8e1d05950 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -77,6 +77,7 @@ internal sealed class SendStates { val rate: BigDecimal?, val appCurrency: AppCurrency, val isFeeApproximate: Boolean, + val isCustomSelected: Boolean, val notifications: ImmutableList, ) : SendStates() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index 997c39d2f1..5ef07bb664 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -56,18 +56,26 @@ internal class FeeStateFactory( fun onFeeOnLoadedState(fees: TransactionFee): SendUiState { val state = currentStateProvider() val feeState = state.feeState ?: return state - val feeSelectorState = (feeState.feeSelectorState as? FeeSelectorState.Content)?.copy( + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content + + val isCustomWasSelected = if (feeState.isCustomSelected) { + feeSelectorState?.customValues ?: persistentListOf() + } else { + customFeeFieldConverter.convert(fees.normal) + } + val updatedFeeSelectorState = feeSelectorState?.copy( fees = fees, + customValues = isCustomWasSelected, ) ?: FeeSelectorState.Content( fees = fees, customValues = customFeeFieldConverter.convert(fees.normal), ) - val fee = feeConverter.convert(feeSelectorState) + val fee = feeConverter.convert(updatedFeeSelectorState) return state.copy( amountState = state.amountState?.copy(isFeeLoading = false), feeState = feeState.copy( - feeSelectorState = feeSelectorState, + feeSelectorState = updatedFeeSelectorState, fee = fee, isFeeApproximate = isFeeApproximate(fee), ), @@ -91,9 +99,11 @@ internal class FeeStateFactory( val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType) val fee = feeConverter.convert(updatedFeeSelectorState) + val isCustomFeeWasSelected = feeState.isCustomSelected || updatedFeeSelectorState.selectedFee == FeeType.Custom return state.copy( feeState = feeState.copy( fee = fee, + isCustomSelected = isCustomFeeWasSelected, feeSelectorState = updatedFeeSelectorState, ), ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt index caf0ee5683..8bab03a2c8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -20,6 +20,7 @@ internal class SendFeeStateConverter( rate = feeCryptoCurrencyStatusProvider().value.fiatRate, appCurrency = appCurrencyProvider(), isFeeApproximate = false, + isCustomSelected = false, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt index c07c45a39b..766380d826 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt @@ -48,6 +48,7 @@ internal object FeeStatePreviewData { ), isFeeApproximate = false, notifications = persistentListOf(), + isCustomSelected = false, ) val errorFeeState = feeState.copy( From 38d95a7a9cbc82b346dc1a45a80375745944b0bd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Apr 2024 11:33:39 +0500 Subject: [PATCH 075/109] Updated on 2026-08-14 --- .../AmountVisualTransformation.kt | 12 ++--- .../core/ui/utils/BigDecimalFormatter.kt | 48 +++++++++---------- .../fields/SendAmountFieldChangeConverter.kt | 9 +++- .../impl/presentation/ui/send/AmountBlock.kt | 10 ++-- 4 files changed, 38 insertions(+), 41 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt index 5ef8a2c4e3..8f10352030 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt @@ -7,7 +7,6 @@ import androidx.compose.ui.text.input.VisualTransformation import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.defaultFormat import com.tangem.core.ui.utils.formatWithThousands -import com.tangem.core.ui.utils.parseToBigDecimal import java.text.DecimalFormat class AmountVisualTransformation( @@ -23,21 +22,16 @@ class AmountVisualTransformation( decimals, ) formattedAmount = formattedAmount.ifEmpty { decimalFormat.defaultFormat() } - val decimalValue = text.text.parseToBigDecimal(decimals) val formattedText = if (formattedAmount.isNotEmpty() && symbol != null) { AnnotatedString( if (currencyCode != null) { - BigDecimalFormatter.formatFiatAmount( - fiatAmount = decimalValue, + BigDecimalFormatter.formatFiatEditableAmount( + fiatAmount = formattedAmount, fiatCurrencyCode = currencyCode, fiatCurrencySymbol = symbol, ) } else { - BigDecimalFormatter.formatCryptoAmountUncapped( - cryptoAmount = decimalValue, - cryptoSymbol = symbol, - decimals = decimals, - ) + BigDecimalFormatter.formatWithSymbol(formattedAmount, symbol) }, ) } else { diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 3ca0b57adf..42309223b1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -1,8 +1,10 @@ package com.tangem.core.ui.utils import com.tangem.domain.tokens.model.CryptoCurrency +import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode +import java.text.DecimalFormat import java.text.NumberFormat import java.util.Currency import java.util.Locale @@ -38,30 +40,6 @@ object BigDecimalFormatter { } } - fun formatCryptoAmountUncapped( - cryptoAmount: BigDecimal?, - cryptoSymbol: String, - decimals: Int, - locale: Locale = Locale.getDefault(), - ): String { - if (cryptoAmount == null) return EMPTY_BALANCE_SIGN - - val formatter = NumberFormat.getNumberInstance(locale).apply { - maximumFractionDigits = decimals - minimumFractionDigits = minOf(2, decimals) - isGroupingUsed = true - roundingMode = RoundingMode.DOWN - } - - return formatter.format(cryptoAmount).let { - if (cryptoSymbol.isEmpty()) { - it - } else { - it + "\u2009$cryptoSymbol" - } - } - } - fun formatCryptoAmount( cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency, @@ -90,6 +68,26 @@ object BigDecimalFormatter { .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) } + fun formatFiatEditableAmount( + fiatAmount: String?, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + locale: Locale = Locale.getDefault(), + ): String { + if (fiatAmount == null) return EMPTY_BALANCE_SIGN + + val formatterCurrency = getCurrency(fiatCurrencyCode) + val numberFormatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + } + val formatter = requireNotNull(numberFormatter as? DecimalFormat) { + Timber.e("NumberFormat is null") + return EMPTY_BALANCE_SIGN + } + return "${formatter.positivePrefix}$fiatAmount${formatter.positiveSuffix}" + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + } + fun formatPercent( percent: BigDecimal, useAbsoluteValue: Boolean, @@ -107,6 +105,8 @@ object BigDecimalFormatter { return formatter.format(value) } + fun formatWithSymbol(amount: String, symbol: String) = "$amount\u2009$symbol" + private fun getCurrency(code: String): Currency { return runCatching { Currency.getInstance(code) } .getOrElse { e -> diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index 273e457783..a1fc8fbe25 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -45,7 +45,7 @@ internal class SendAmountFieldChangeConverter( cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), keyboardOptions = KeyboardOptions( - imeAction = if (!isExceedBalance) ImeAction.Done else ImeAction.None, + imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), keyboardType = KeyboardType.Number, ), ), @@ -103,4 +103,11 @@ internal class SendAmountFieldChangeConverter( cryptoDecimal > currencyCryptoAmount } } + + private fun getKeyboardAction(isExceedBalance: Boolean, decimalCryptoValue: BigDecimal) = + if (!isExceedBalance && !decimalCryptoValue.isZero()) { + ImeAction.Done + } else { + ImeAction.None + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt index 528cdfea00..85ff055ed6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt @@ -30,13 +30,9 @@ internal fun AmountBlock( ) { val amount = amountState.amountTextField - val cryptoAmount = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = amount.cryptoAmount.value, - cryptoCurrency = amount.cryptoAmount.currencySymbol, - decimals = amount.cryptoAmount.decimals, - ) - val fiatAmount = BigDecimalFormatter.formatFiatAmount( - fiatAmount = amount.fiatAmount.value, + val cryptoAmount = BigDecimalFormatter.formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) + val fiatAmount = BigDecimalFormatter.formatFiatEditableAmount( + fiatAmount = amount.fiatValue, fiatCurrencySymbol = amount.fiatAmount.currencySymbol, fiatCurrencyCode = amountState.appCurrencyCode, ) From df7e72f5275fa8d8cad4e47408b398a3a34bf561 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Apr 2024 13:40:33 +0500 Subject: [PATCH 076/109] Updated on 2026-08-14 --- .../presentation/state/SendStateFactory.kt | 2 +- .../presentation/state/fee/FeeConverter.kt | 4 ++-- .../presentation/state/fee/FeeStateFactory.kt | 4 ++-- .../state/fee/SendFeeCustomFieldConverter.kt | 4 ++-- .../state/fee/SendFeeStateConverter.kt | 4 ++-- .../fee/custom/EthereumCustomFeeConverter.kt | 23 +++++++++--------- .../presentation/ui/SendNavigationButtons.kt | 2 +- .../presentation/ui/send/RecipientBlock.kt | 24 ------------------- .../presentation/viewmodel/SendViewModel.kt | 8 +++---- 9 files changed, 26 insertions(+), 49 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index f50a061117..7a5c113219 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -33,7 +33,7 @@ internal class SendStateFactory( private val userWalletProvider: Provider, private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, - private val feeCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, private val isTapHelpPreviewEnabledProvider: Provider, private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt index bd8e832a91..2d876ae4e9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt @@ -13,10 +13,10 @@ import com.tangem.utils.converter.Converter internal class FeeConverter( private val clickIntents: SendClickIntents, private val appCurrencyProvider: Provider, - private val feeCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter { - private val ethereumCustomFeeConverter by lazy { + private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { EthereumCustomFeeConverter( clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index 5ef07bb664..ba181f423f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -21,7 +21,7 @@ import kotlinx.collections.immutable.persistentListOf internal class FeeStateFactory( private val clickIntents: SendClickIntents, private val currentStateProvider: Provider, - private val feeCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, private val isFeeApproximateUseCase: IsFeeApproximateUseCase, ) { @@ -153,7 +153,7 @@ internal class FeeStateFactory( } private fun isFeeApproximate(fee: Fee): Boolean { - val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() + val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() ?: return false return isFeeApproximateUseCase( networkId = cryptoCurrencyStatus.currency.network.id, amountType = fee.amount.type, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt index cdd0fa60e6..79c7a7e318 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -14,10 +14,10 @@ import kotlinx.collections.immutable.persistentListOf internal class SendFeeCustomFieldConverter( private val clickIntents: SendClickIntents, private val appCurrencyProvider: Provider, - private val feeCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter> { - private val ethereumCustomFeeConverter by lazy { + private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { EthereumCustomFeeConverter( clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt index 8bab03a2c8..d9b92b387d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -9,7 +9,7 @@ import kotlinx.collections.immutable.persistentListOf internal class SendFeeStateConverter( private val appCurrencyProvider: Provider, - private val feeCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter { override fun convert(value: Unit): SendStates.FeeState { @@ -17,7 +17,7 @@ internal class SendFeeStateConverter( feeSelectorState = FeeSelectorState.Error, fee = null, notifications = persistentListOf(), - rate = feeCryptoCurrencyStatusProvider().value.fiatRate, + rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate, appCurrency = appCurrencyProvider(), isFeeApproximate = false, isCustomSelected = false, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt index 11dfc8f056..97726412f7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -28,7 +28,7 @@ import java.math.RoundingMode internal class EthereumCustomFeeConverter( private val clickIntents: SendClickIntents, private val appCurrencyProvider: Provider, - private val feeCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter> { override fun convert(value: Fee.Ethereum): ImmutableList { @@ -49,8 +49,8 @@ internal class EthereumCustomFeeConverter( keyboardActions = KeyboardActions(), ), SendTextField.CustomFee( - value = value.gasPrice.toString(), - decimals = GAS_DECIMALS, + value = value.gasPrice.toBigDecimal().movePointLeft(GIGA_DECIMALS).toString(), + decimals = GIGA_DECIMALS, symbol = ETHEREUM_GAS_UNIT, title = resourceReference(R.string.send_gas_price), footer = resourceReference(R.string.send_gas_price_footer), @@ -105,7 +105,7 @@ internal class EthereumCustomFeeConverter( private fun getFeeFormatted(fee: BigDecimal?): TextReference { val appCurrency = appCurrencyProvider() - val rate = feeCryptoCurrencyStatusProvider().value.fiatRate + val rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate val fiatFee = rate?.let { fee?.multiply(it) } return stringReference( BigDecimalFormatter.formatFiatAmount( @@ -118,7 +118,7 @@ internal class EthereumCustomFeeConverter( private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean { val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() - val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + val currencyCryptoAmount = cryptoCurrencyStatus?.value?.amount ?: BigDecimal.ZERO return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount } @@ -134,9 +134,9 @@ internal class EthereumCustomFeeConverter( setEmpty(GAS_PRICE) } else { val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals) - val newFeeAmount = newFeeAmountDecimal.movePointRight(this[FEE_AMOUNT].decimals) - val newGasPrice = newFeeAmount.divide(gasLimit, GAS_DECIMALS, RoundingMode.HALF_UP) - set(GAS_PRICE, this[GAS_PRICE].copy(value = newGasPrice.parseBigDecimal(GAS_DECIMALS))) + val newFeeAmount = newFeeAmountDecimal.movePointRight(this[GAS_PRICE].decimals) // from ETH to GWEI + val newGasPrice = newFeeAmount.divide(gasLimit, this[GAS_PRICE].decimals, RoundingMode.HALF_UP) + set(GAS_PRICE, this[GAS_PRICE].copy(value = newGasPrice.parseBigDecimal(this[GAS_PRICE].decimals))) set( index, this[index].copy( @@ -154,8 +154,8 @@ internal class EthereumCustomFeeConverter( setEmpty(GAS_PRICE) } else { val newGasPrice = value.parseToBigDecimal(this[GAS_PRICE].decimals) - .movePointLeft(this[GAS_PRICE].decimals) - val newFeeAmount = (gasLimit * newGasPrice).movePointLeft(this[FEE_AMOUNT].decimals) + .movePointLeft(this[GAS_PRICE].decimals) // from GWEI to ETH + val newFeeAmount = gasLimit * newGasPrice set( FEE_AMOUNT, this[FEE_AMOUNT].copy( @@ -174,7 +174,7 @@ internal class EthereumCustomFeeConverter( } else { val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals) val gasPrice = this[GAS_PRICE].value.parseToBigDecimal(this[GAS_PRICE].decimals) - .movePointLeft(this[FEE_AMOUNT].decimals) + .movePointLeft(this[GAS_PRICE].decimals) // from GWEI to ETH val newFeeAmount = newGasLimit * gasPrice set( FEE_AMOUNT, @@ -198,6 +198,7 @@ internal class EthereumCustomFeeConverter( companion object { private const val ETHEREUM_GAS_UNIT = "GWEI" + private const val GIGA_DECIMALS = 9 private const val FEE_AMOUNT = 0 private const val GAS_PRICE = 1 private const val GAS_LIMIT = 2 diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 5bdb10824e..20f2800793 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -140,7 +140,7 @@ private fun SendingText(uiState: SendUiState, isVisible: Boolean, modifier: Modi val feeState = uiState.feeState val fiatRate = feeState?.rate val fiatAmount = amountState?.amountTextField?.fiatAmount - val feeFiat = feeState?.fee?.amount?.value?.multiply(fiatRate) + val feeFiat = fiatRate?.let { feeState.fee?.amount?.value?.multiply(it) } val sendingFiat = feeFiat?.let { fiatAmount?.value?.plus(it) } if (feeFiat != null && sendingFiat != null) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt index 473d043d35..f276edf3c1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -41,7 +40,6 @@ internal fun RecipientBlock( .padding(TangemTheme.dimens.spacing12), ) { AddressBlock(recipientState.addressTextField) - MemoBlock(recipientState.memoTextField) } } @@ -72,28 +70,6 @@ private fun AddressBlock(address: SendTextField.RecipientAddress) { } } -@Composable -private fun MemoBlock(memo: SendTextField.RecipientMemo?) { - val showMemo = memo != null && memo.value.isNotBlank() - if (showMemo) { - HorizontalDivider( - color = TangemTheme.colors.stroke.primary, - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), - ) - Text( - text = memo?.label?.resolveReference().orEmpty(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, - ) - Text( - text = memo?.value.orEmpty(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), - ) - } -} - // region Preview @Preview @Composable diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 8ea9eebab5..df9aa391e9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -184,7 +184,7 @@ internal class SendViewModel @Inject constructor( private var isTapHelpPreviewEnabled: Boolean = false private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() - private var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null private var balanceJobHolder = JobHolder() private var balanceHidingJobHolder = JobHolder() @@ -328,12 +328,12 @@ internal class SendViewModel @Inject constructor( private suspend fun getFeeCurrencyStatusSync( cryptoCurrencyStatus: CryptoCurrencyStatus, isMultiCurrency: Boolean, - ): CryptoCurrencyStatus { + ): CryptoCurrencyStatus? { return if (isMultiCurrency) { getFeePaidCryptoCurrencyStatusSyncUseCase( userWalletId = userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, - ).getOrNull() ?: error("Fee currency is unreachable") + ).getOrNull() } else { cryptoCurrencyStatus } @@ -354,7 +354,7 @@ internal class SendViewModel @Inject constructor( private fun onDataLoaded( currencyStatus: CryptoCurrencyStatus, coinCurrencyStatus: CryptoCurrencyStatus, - feeCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, ) { cryptoCurrencyStatus = currencyStatus coinCryptoCurrencyStatus = coinCurrencyStatus From 7e8b587fbb76ea59c197d5c366f6ddaa57d2e1e7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Apr 2024 15:30:28 +0500 Subject: [PATCH 077/109] Updated on 2026-08-14 --- .../send/impl/presentation/viewmodel/SendViewModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index df9aa391e9..4670b152cc 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -412,7 +412,9 @@ internal class SendViewModel @Inject constructor( private suspend fun UserWallet.toAvailableWallet(): AvailableWallet? { return if (!isMultiCurrency) { val status = getCryptoCurrencyStatusSyncUseCase(walletId).getOrNull() - val address = status?.value?.networkAddress + val address = status?.value?.networkAddress.takeIf { + status?.currency?.network?.id == cryptoCurrency.network.id + } address?.let { AvailableWallet( name = name, From c68c5db506b10f46389b33c22d5778d3f76e4e7a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Apr 2024 15:31:30 +0500 Subject: [PATCH 078/109] Updated on 2026-08-14 --- .../impl/presentation/state/SendAlertState.kt | 8 ++++ .../state/SendEventStateFactory.kt | 14 ++++++ .../state/amount/AmountNotificationFactory.kt | 38 ---------------- .../state/previewdata/SendClickIntentsStub.kt | 2 +- .../viewmodel/SendClickIntents.kt | 2 +- .../presentation/viewmodel/SendViewModel.kt | 43 ++++++++----------- 6 files changed, 41 insertions(+), 66 deletions(-) delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountNotificationFactory.kt diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt index 93bb812fae..d3adc2e74c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt @@ -75,4 +75,12 @@ internal sealed class SendAlertState { override val message: TextReference = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text) } + + data class FeeUnreachableError( + override val onConfirmClick: (() -> Unit), + ) : SendAlertState() { + override val title: TextReference = resourceReference(R.string.send_fee_unreachable_error_title) + override val message: TextReference = resourceReference(R.string.send_fee_unreachable_error_text) + override val confirmButtonText = resourceReference(R.string.warning_button_refresh) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt index 2928e3d1f6..58e0a97b65 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt @@ -123,4 +123,18 @@ internal class SendEventStateFactory( ), ) } + + fun getFeeUnreachableErrorState(onConsume: () -> Unit): SendUiState { + val state = currentStateProvider() + return state.copy( + event = triggeredEvent( + data = SendEvent.ShowAlert( + SendAlertState.FeeUnreachableError( + onConfirmClick = { clickIntents.feeReload(true) }, + ), + ), + onConsume = onConsume, + ), + ) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountNotificationFactory.kt deleted file mode 100644 index 222a8704cf..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountNotificationFactory.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.features.send.impl.presentation.state.amount - -import com.tangem.features.send.impl.presentation.state.SendUiState -import com.tangem.features.send.impl.presentation.state.SendUiStateType -import com.tangem.features.send.impl.presentation.state.StateRouter -import com.tangem.features.send.impl.presentation.state.SendNotification -import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState -import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents -import com.tangem.utils.Provider -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map - -internal class AmountNotificationFactory( - private val stateRouterProvider: Provider, - private val currentStateProvider: Provider, - private val clickIntents: SendClickIntents, -) { - - fun create() = stateRouterProvider().currentState - .filter { it.type == SendUiStateType.Amount } - .map { - buildList { - addFeeUnreachableNotification() - }.toImmutableList() - } - - private fun MutableList.addFeeUnreachableNotification() { - val state = currentStateProvider() - val feeState = state.feeState ?: return - - if (feeState.feeSelectorState is FeeSelectorState.Error) { - add( - SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload), - ) - } - } -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt index d70b281ffe..67d5b07642 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt @@ -33,7 +33,7 @@ internal object SendClickIntentsStub : SendClickIntents { override fun onRecipientMemoValueChange(value: String) {} - override fun feeReload() {} + override fun feeReload(isToNextState: Boolean) {} override fun onFeeSelectorClick(feeType: FeeType) {} diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index c35cf2ba2d..14ed1562af 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -38,7 +38,7 @@ internal interface SendClickIntents { // endregion // region Fee - fun feeReload() + fun feeReload(isToNextState: Boolean = false) fun onFeeSelectorClick(feeType: FeeType) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 4670b152cc..099bcd007f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -46,7 +46,6 @@ import com.tangem.features.send.impl.presentation.analytics.SendScreenSource import com.tangem.features.send.impl.presentation.analytics.utils.SendOnNextScreenAnalyticSender import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.* -import com.tangem.features.send.impl.presentation.state.amount.AmountNotificationFactory import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactory import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory import com.tangem.features.send.impl.presentation.state.fee.* @@ -147,12 +146,6 @@ internal class SendViewModel @Inject constructor( feeStateFactory = feeStateFactory, ) - private val amountNotificationFactory = AmountNotificationFactory( - currentStateProvider = Provider { uiState }, - stateRouterProvider = Provider { stateRouter }, - clickIntents = this, - ) - private val feeNotificationFactory = FeeNotificationFactory( currentStateProvider = Provider { uiState }, stateRouterProvider = Provider { stateRouter }, @@ -194,7 +187,6 @@ internal class SendViewModel @Inject constructor( private var memoValidationJobHolder = JobHolder() private var sendNotificationsJobHolder = JobHolder() private var feeNotificationsJobHolder = JobHolder() - private var amountNotificationsJobHolder = JobHolder() private var qrScannerJobHolder = JobHolder() private var sendIdleTimer = 0L @@ -478,16 +470,6 @@ internal class SendViewModel @Inject constructor( .saveIn(feeNotificationsJobHolder) } - private fun updateAmountNotifications() { - amountNotificationFactory.create() - .conflate() - .distinctUntilChanged() - .onEach { uiState = amountStateFactory.getAmountNotificationState(notifications = it) } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(amountNotificationsJobHolder) - } - // region screen state navigation override fun popBackStack() = stateRouter.popBackStack() override fun onBackClick() { @@ -661,7 +643,7 @@ internal class SendViewModel @Inject constructor( // endregion // region fee - override fun feeReload() = loadFee() + override fun feeReload(isToNextState: Boolean) = loadFee(isToNextState = isToNextState) override fun onFeeSelectorClick(feeType: FeeType) { uiState = feeStateFactory.onFeeSelectedState(feeType) @@ -706,20 +688,30 @@ internal class SendViewModel @Inject constructor( } }, ifLeft = { - if (isShowStatus) uiState = feeStateFactory.onFeeOnErrorState() + onFeeLoadFailed(isShowStatus, isToNextState) }, ) - if (result == null && isShowStatus) { - uiState = feeStateFactory.onFeeOnErrorState() + if (result == null) { + onFeeLoadFailed(isShowStatus, isToNextState) } updateFeeNotifications() - updateAmountNotifications() }.saveIn(feeJobHolder) .invokeOnCompletion { uiState = amountStateFactory.getOnAmountFeeLoadingCancel() } } + private fun onFeeLoadFailed(isShowStatus: Boolean, isToNextState: Boolean) { + when { + isToNextState -> { + uiState = eventStateFactory.getFeeUnreachableErrorState { + uiState = eventStateFactory.onConsumeEventState() + } + } + isShowStatus -> uiState = feeStateFactory.onFeeOnErrorState() + } + } + private suspend fun checkIfSubtractAvailable() { isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, cryptoCurrency).fold( ifRight = { it }, @@ -884,8 +876,7 @@ internal class SendViewModel @Inject constructor( }, ifLeft = { uiState = stateFactory.getSendingStateUpdate(isSending = false) - eventStateFactory.getGenericErrorState( - error = (it as? GetFeeError.DataError)?.cause, + eventStateFactory.getFeeUnreachableErrorState( onConsume = { uiState = eventStateFactory.onConsumeEventState() }, ) }, @@ -895,7 +886,7 @@ internal class SendViewModel @Inject constructor( feeUpdatedState } else { uiState = stateFactory.getSendingStateUpdate(isSending = false) - eventStateFactory.getGenericErrorState( + eventStateFactory.getFeeUnreachableErrorState( onConsume = { uiState = eventStateFactory.onConsumeEventState() }, ) } From 65743285e41e60b65ccc554c8a8d33151be0f3ad Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Apr 2024 15:31:30 +0500 Subject: [PATCH 079/109] Updated on 2026-08-14 --- .../features/send/impl/presentation/state/SendAlertState.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt index d3adc2e74c..42a3494b50 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt @@ -12,7 +12,9 @@ internal sealed class SendAlertState { abstract val title: TextReference? abstract val message: TextReference open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + // open val cancelButtonText: TextReference = resourceReference(id = R.string.common_cancel) open val onConfirmClick: (() -> Unit)? = null + // open val onCancelClick: (() -> Unit)? = null data class GenericError( override val title: TextReference? = resourceReference(id = R.string.send_alert_transaction_failed_title), From 16b8fae4d75789e7a08356bad7821d1a2748d1b1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Apr 2024 17:21:35 +0500 Subject: [PATCH 080/109] Updated on 2026-08-14 --- .../inputrow/InputRowEnterInfoAmount.kt | 2 + .../core/ui/utils/DecimalFormatterExt.kt | 10 +- .../impl/presentation/state/SendAlertState.kt | 2 - .../presentation/state/fee/FeeConverter.kt | 10 ++ .../state/fee/SendFeeCustomFieldConverter.kt | 22 ++- .../fee/custom/BitcoinCustomFeeConverter.kt | 139 ++++++++++++++++++ .../state/fee/custom/CustomFeeConverter.kt | 10 ++ .../fee/custom/EthereumCustomFeeConverter.kt | 5 +- .../state/fields/SendTextField.kt | 1 + ...dCustomFeeEthereum.kt => SendCustomFee.kt} | 3 +- .../ui/fee/SendSpeedAndFeeContent.kt | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 2 +- gradle/dependencies.toml | 2 +- 13 files changed, 195 insertions(+), 15 deletions(-) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/CustomFeeConverter.kt rename features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/{SendCustomFeeEthereum.kt => SendCustomFee.kt} (97%) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt index 00d4221e37..f3d8a0c1cc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt @@ -57,6 +57,7 @@ fun InputRowEnterInfoAmount( keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, showDivider: Boolean = false, + isReadOnly: Boolean = false, ) { DividerContainer( modifier = modifier, @@ -83,6 +84,7 @@ fun InputRowEnterInfoAmount( ), onValueChange = onValueChange, color = textColor, + isEnabled = !isReadOnly, textStyle = TangemTheme.typography.body2, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt index 8cc2cf7bda..7db27cf7f4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt @@ -59,7 +59,7 @@ fun DecimalFormat.getValidatedNumberWithFixedDecimals(text: String, decimals: In return if (filteredChars.count { it == decimalSeparator } == 1) { val beforeDecimal = filteredChars.substringBefore(decimalSeparator) val afterDecimal = filteredChars.substringAfter(decimalSeparator) - beforeDecimal + decimalSeparator + afterDecimal.take(decimals) + decimals.getWithIntegerDecimals(beforeDecimal, decimalSeparator, afterDecimal) } // If there is no dot, just take all digits else { @@ -82,7 +82,7 @@ fun DecimalFormat.formatWithThousands(text: String, decimals: Int): String { .joinToString(thousandsSeparator.toString()) .reversed() val afterDecimal = localizedText.substringAfter(decimalSeparator) - beforeDecimal + decimalSeparator + afterDecimal.take(decimals) + decimals.getWithIntegerDecimals(beforeDecimal, decimalSeparator, afterDecimal) } // If there is no dot, just take all digits else { @@ -150,4 +150,10 @@ fun BigDecimal.parseBigDecimal(decimals: Int, roundingMode: RoundingMode = Round } catch (e: Exception) { "" } +} + +private fun Int.getWithIntegerDecimals(before: String, separator: Char, after: String): String = if (this == 0) { + before +} else { + before + separator + after.take(this) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt index 42a3494b50..d3adc2e74c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt @@ -12,9 +12,7 @@ internal sealed class SendAlertState { abstract val title: TextReference? abstract val message: TextReference open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - // open val cancelButtonText: TextReference = resourceReference(id = R.string.common_cancel) open val onConfirmClick: (() -> Unit)? = null - // open val onCancelClick: (() -> Unit)? = null data class GenericError( override val title: TextReference? = resourceReference(id = R.string.send_alert_transaction_failed_title), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt index 2d876ae4e9..1fa99ee291 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.fee.custom.BitcoinCustomFeeConverter import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider @@ -24,6 +25,14 @@ internal class FeeConverter( ) } + private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { + BitcoinCustomFeeConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, + ) + } + override fun convert(value: FeeSelectorState.Content): Fee { return when (val fees = value.fees) { is TransactionFee.Choosable -> { @@ -46,6 +55,7 @@ internal class FeeConverter( } else { when (normalFee) { is Fee.Ethereum -> ethereumCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues) + is Fee.Bitcoin -> bitcoinCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues) else -> { val customFee = customValues.firstOrNull() Fee.Common( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt index 79c7a7e318..c9534ea97f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.state.fee import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.fee.custom.BitcoinCustomFeeConverter import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents @@ -25,22 +26,35 @@ internal class SendFeeCustomFieldConverter( ) } + private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { + BitcoinCustomFeeConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, + ) + } + override fun convert(value: Fee): ImmutableList { return when (value) { - is Fee.Ethereum -> { - ethereumCustomFeeConverter.convert(value) - } + is Fee.Ethereum -> ethereumCustomFeeConverter.convert(value) + is Fee.Bitcoin -> bitcoinCustomFeeConverter.convert(value) else -> persistentListOf() } } fun onValueChange(feeSelectorState: FeeSelectorState.Content, index: Int, value: String) = feeSelectorState.copy( - customValues = when (feeSelectorState.fees.normal) { + customValues = when (val fee = feeSelectorState.fees.normal) { is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange( customValues = feeSelectorState.customValues, index = index, value = value, ) + is Fee.Bitcoin -> bitcoinCustomFeeConverter.onValueChange( + customValues = feeSelectorState.customValues, + index = index, + value = value, + txSize = fee.txSize, + ) else -> feeSelectorState.customValues }, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt new file mode 100644 index 0000000000..6fb6ce20ef --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt @@ -0,0 +1,139 @@ +package com.tangem.features.send.impl.presentation.state.fee.custom + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.extensions.isZero +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal +import java.math.RoundingMode + +internal class BitcoinCustomFeeConverter( + private val clickIntents: SendClickIntents, + private val appCurrencyProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, +) : CustomFeeConverter { + + override fun convert(value: Fee.Bitcoin): ImmutableList { + val feeValue = value.amount.value + return persistentListOf( + SendTextField.CustomFee( + value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(), + decimals = value.amount.decimals, + symbol = value.amount.currencySymbol, + onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + title = resourceReference(R.string.send_max_fee), + footer = resourceReference(R.string.send_max_fee_footer), + label = getFeeFormatted(feeValue), + keyboardActions = KeyboardActions(), + isReadonly = true, + ), + SendTextField.CustomFee( + value = toSatoshiPerByte( + amount = feeValue, + decimals = value.amount.decimals, + txSize = value.txSize, + ).toString(), + decimals = SATOSHI_DECIMALS, + symbol = "", + title = resourceReference(R.string.send_gas_price), + footer = resourceReference(R.string.send_gas_price_footer), + onValueChange = { clickIntents.onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) }, + keyboardOptions = KeyboardOptions( + imeAction = if (checkExceedBalance(feeValue)) ImeAction.None else ImeAction.Done, + keyboardType = KeyboardType.Number, + ), + keyboardActions = KeyboardActions(), + ), + ) + } + + override fun convertBack(normalFee: Fee.Bitcoin, value: ImmutableList): Fee.Bitcoin { + val feeAmount = value[FEE_AMOUNT_INDEX].value.parseToBigDecimal(value[FEE_AMOUNT_INDEX].decimals) + val satoshiPerByte = value[FEE_SATOSHI_INDEX].value.parseToBigDecimal(value[FEE_SATOSHI_INDEX].decimals) + return normalFee.copy( + amount = normalFee.amount.copy(value = feeAmount), + satoshiPerByte = satoshiPerByte, + ) + } + + fun onValueChange( + customValues: ImmutableList, + index: Int, + value: String, + txSize: BigDecimal, + ): ImmutableList { + val mutableCustomValues = customValues.toMutableList() + return mutableCustomValues.apply { + if (index == FEE_SATOSHI_INDEX) { + val newSatoshiPerKb = value.parseToBigDecimal(this[FEE_SATOSHI_INDEX].decimals) + val newFeeAmount = newSatoshiPerKb.multiply(txSize) + .movePointLeft(this[FEE_AMOUNT_INDEX].decimals) + .setScale(this[FEE_AMOUNT_INDEX].decimals, RoundingMode.DOWN) + set( + FEE_AMOUNT_INDEX, + this[FEE_AMOUNT_INDEX].copy( + value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT_INDEX].decimals), + label = getFeeFormatted(newFeeAmount), + ), + ) + set(index, this[index].copy(value = value)) + } + }.toImmutableList() + } + + private fun getFeeFormatted(fee: BigDecimal?): TextReference { + val appCurrency = appCurrencyProvider() + val rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate + val fiatFee = rate?.let { fee?.multiply(it) } + return stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatFee, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ) + } + + private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean { + val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() + val currencyCryptoAmount = cryptoCurrencyStatus?.value?.amount ?: BigDecimal.ZERO + + return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount + } + + private fun toSatoshiPerByte(amount: BigDecimal?, decimals: Int, txSize: BigDecimal): BigDecimal? { + val newFeeAmount = amount?.movePointRight(decimals) + return newFeeAmount?.divide( + txSize, + SATOSHI_DECIMALS, + RoundingMode.HALF_UP, + )?.setScale(SATOSHI_DECIMALS, RoundingMode.HALF_UP) + } + + private companion object { + private const val FEE_AMOUNT_INDEX = 0 + private const val FEE_SATOSHI_INDEX = 1 + private const val SATOSHI_DECIMALS = 0 + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/CustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/CustomFeeConverter.kt new file mode 100644 index 0000000000..25764269ec --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/CustomFeeConverter.kt @@ -0,0 +1,10 @@ +package com.tangem.features.send.impl.presentation.state.fee.custom + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList + +internal interface CustomFeeConverter : Converter> { + fun convertBack(normalFee: T, value: ImmutableList): T +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt index 97726412f7..a9bf8e5607 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -18,7 +18,6 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -29,7 +28,7 @@ internal class EthereumCustomFeeConverter( private val clickIntents: SendClickIntents, private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatusProvider: Provider, -) : Converter> { +) : CustomFeeConverter { override fun convert(value: Fee.Ethereum): ImmutableList { val feeValue = value.amount.value @@ -77,7 +76,7 @@ internal class EthereumCustomFeeConverter( ) } - fun convertBack(normalFee: Fee.Ethereum, value: ImmutableList): Fee.Ethereum { + override fun convertBack(normalFee: Fee.Ethereum, value: ImmutableList): Fee.Ethereum { val feeAmount = value[FEE_AMOUNT].value.parseToBigDecimal(value[FEE_AMOUNT].decimals) val gasPrice = value[GAS_PRICE].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger() val gasLimit = value[GAS_LIMIT].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt index 8db8a2fc02..266f9e8f6e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt @@ -64,5 +64,6 @@ internal sealed class SendTextField { val title: TextReference, val footer: TextReference, val label: TextReference? = null, + val isReadonly: Boolean = false, ) : SendTextField() } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt similarity index 97% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt rename to features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt index 50fde69ede..79cff9da25 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt @@ -17,7 +17,7 @@ import com.tangem.features.send.impl.presentation.ui.common.FooterContainer import kotlinx.collections.immutable.ImmutableList @Composable -internal fun SendCustomFeeEthereum( +internal fun SendCustomFee( customValues: ImmutableList, selectedFee: FeeType, hasNotifications: Boolean, @@ -55,6 +55,7 @@ internal fun SendCustomFeeEthereum( keyboardActions = value.keyboardActions, onValueChange = value.onValueChange, showDivider = false, + isReadOnly = value.isReadonly, modifier = Modifier .background( color = TangemTheme.colors.background.action, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt index ee4055bff5..6281bfd810 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -64,7 +64,7 @@ internal fun LazyListScope.customFee( item( key = FEE_CUSTOM_KEY, ) { - SendCustomFeeEthereum( + SendCustomFee( customValues = feeSendState.customValues, selectedFee = feeSendState.selectedFee, hasNotifications = hasNotifications, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 08b31e9a2e..966b619bf8 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1406,10 +1406,10 @@ internal class SwapInteractorImpl @Inject constructor( private fun Fee.getGasLimit(): Int { return when (this) { - is Fee.Common -> 0 is Fee.Ethereum -> gasLimit.toInt() is Fee.VeChain -> gasLimit.toInt() is Fee.Aptos -> gasLimit.toInt() + else -> 0 } } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 8731df13aa..d02fccec51 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,7 +85,7 @@ leakcanary = "2.13" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.9-560" +tangemBlockchainSdk = "release-app_5.9-580" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.9-340" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 524b69f5f5b20b07c85d30071225b9946d4db318 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Apr 2024 15:48:51 +0500 Subject: [PATCH 081/109] Updated on 2026-08-14 --- .../send/impl/presentation/SendFragment.kt | 4 ++- .../impl/presentation/ui/SendEventEffect.kt | 6 ++++ .../send/impl/presentation/ui/SendScreen.kt | 33 ++++++++----------- .../ui/amount/SendAmountContent.kt | 6 +++- .../presentation/ui/common/Notifications.kt | 10 +----- .../impl/presentation/ui/fee/SendCustomFee.kt | 13 ++++---- .../ui/fee/SendSpeedAndFeeContent.kt | 4 ++- .../ui/recipient/SendRecipientContent.kt | 7 ++-- .../impl/presentation/ui/send/SendContent.kt | 6 +++- .../presentation/viewmodel/SendViewModel.kt | 2 +- 10 files changed, 49 insertions(+), 42 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 0a3521b1eb..cebe88712f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import arrow.core.getOrElse @@ -73,7 +74,8 @@ internal class SendFragment : ComposeFragment() { SystemBarsEffect { setSystemBarsColor(systemBarsColor) } - SendScreen(viewModel.uiState, viewModel.stateRouter.currentState) + val currentState = viewModel.stateRouter.currentState.collectAsStateWithLifecycle() + SendScreen(viewModel.uiState, currentState.value) } override fun onDestroy() { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt index 84e638eb8a..53378ca7f7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt @@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.ui import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButton @@ -18,6 +19,11 @@ internal fun SendEventEffect(event: StateEvent, snackbarHostState: Sn val resources = LocalContext.current.resources var alertConfig by remember { mutableStateOf(value = null) } + val keyboardController = LocalSoftwareKeyboardController.current + LaunchedEffect(key1 = alertConfig) { + keyboardController?.hide() + } + alertConfig?.let { SendAlert(state = it, onDismiss = { alertConfig = null }) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index fe28791ec9..55bebe81a7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference @@ -25,11 +24,9 @@ import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent import com.tangem.features.send.impl.presentation.ui.send.SendContent -import kotlinx.coroutines.flow.StateFlow @Composable -internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow) { - val currentState = currentStateFlow.collectAsStateWithLifecycle() +internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) { val snackbarHostState = remember { SnackbarHostState() } val sendState = uiState.sendState ?: return BackHandler { uiState.clickIntents.onBackClick() } @@ -37,11 +34,10 @@ internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow resourceReference(R.string.send_amount_label) SendUiStateType.Recipient -> resourceReference(R.string.send_recipient_label) SendUiStateType.Fee -> resourceReference(R.string.common_fee_selector_title) @@ -52,13 +48,13 @@ internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow null } - val isSending = currentState.value.type == SendUiStateType.Send && !uiState.sendState.isSuccess + val isSending = currentState.type == SendUiStateType.Send && !uiState.sendState.isSuccess val subtitleRes = if (isSending) { uiState.amountState?.walletName } else { null } - val iconRes = if (currentState.value.type == SendUiStateType.Recipient) { + val iconRes = if (currentState.type == SendUiStateType.Recipient) { R.drawable.ic_qrcode_scan_24 } else { null @@ -74,17 +70,16 @@ internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow item::class.java }, contentType = { _, item -> item::class.java }, itemContent = { i, item -> - val bottomPadding = if (i == notifications.lastIndex) { - TangemTheme.dimens.spacing72 - } else { - TangemTheme.dimens.spacing0 - } val topPadding = if (i == 0 && hasPaddingAbove) { TangemTheme.dimens.spacing0 } else { @@ -34,10 +29,7 @@ internal fun LazyListScope.notifications( Notification( config = item.config, modifier = modifier - .padding( - top = topPadding, - bottom = bottomPadding, - ) + .padding(top = topPadding) .animateItemPlacement(), containerColor = when (item) { is SendNotification.Error.ExceedsBalance, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt index 79cff9da25..1c7e332844 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt @@ -29,20 +29,19 @@ internal fun SendCustomFee( enter = expandVertically().plus(fadeIn()), exit = shrinkVertically().plus(fadeOut()), ) { + val bottomPadding = if (hasNotifications) { + TangemTheme.dimens.spacing12 + } else { + TangemTheme.dimens.spacing0 + } Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - modifier = modifier, + modifier = modifier.padding(bottom = bottomPadding), ) { repeat(customValues.size) { index -> val value = customValues[index] - val bottomPadding = if (index == customValues.lastIndex && !hasNotifications) { - TangemTheme.dimens.spacing72 - } else { - TangemTheme.dimens.spacing0 - } FooterContainer( footer = value.footer.resolveReference(), - modifier = Modifier.padding(bottom = bottomPadding), ) { if (value.label != null) { InputRowEnterInfoAmount( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt index 6281bfd810..88f4a37a85 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -31,7 +31,9 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S .fillMaxSize() .background(TangemTheme.colors.background.tertiary) .padding( - horizontal = TangemTheme.dimens.spacing16, + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, ), ) { feeSelector(state, clickIntents) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index 539d82e49c..eb9b2b544a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -51,7 +51,11 @@ internal fun SendRecipientContent( modifier = Modifier .fillMaxSize() .background(TangemTheme.colors.background.tertiary) - .padding(horizontal = TangemTheme.dimens.spacing16), + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), ) { addressItem( address = address, @@ -222,7 +226,6 @@ private fun LazyListScope.listItem( .then( if (isLast && index == list.lastIndex) { Modifier - .padding(bottom = TangemTheme.dimens.spacing72) .clip( shape = RoundedCornerShape( bottomStart = TangemTheme.dimens.radius16, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 78809756c4..0c92c24fd5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -35,7 +35,11 @@ internal fun SendContent(uiState: SendUiState) { LazyColumn( modifier = Modifier .fillMaxSize() - .padding(horizontal = TangemTheme.dimens.spacing16), + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), ) { blocks(uiState) tapHelp(isDisplay = sendState.showTapHelp) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 099bcd007f..069a34f6a3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -465,7 +465,7 @@ internal class SendViewModel @Inject constructor( .conflate() .distinctUntilChanged() .onEach { uiState = feeStateFactory.getFeeNotificationState(notifications = it) } - .flowOn(dispatchers.io) + .flowOn(dispatchers.main) .launchIn(viewModelScope) .saveIn(feeNotificationsJobHolder) } From 56baebe95c651d82c3031e6bc4d78f2b6837dd23 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Apr 2024 15:51:35 +0500 Subject: [PATCH 082/109] Updated on 2026-08-14 --- .../com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 6e18152f25..eded0146fc 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -197,7 +197,7 @@ class GetCurrencyWarningsUseCase( coinCurrency = coinStatus.currency, ) } - feePaidCurrency is FeePaidCurrency.SameCurrency && !tokenStatus.value.amount.isZero() -> { + feePaidCurrency is FeePaidCurrency.SameCurrency && tokenStatus.value.amount.isZero() -> { CryptoCurrencyWarning.BalanceNotEnoughForFee( tokenCurrency = tokenStatus.currency, coinCurrency = coinStatus.currency, From 23b9a7bfd1286a9e610a52bdea385631b9ed2a8a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 5 Apr 2024 15:32:12 +0100 Subject: [PATCH 083/109] Updated on 2026-08-14 --- app/src/main/assets/testnet_tokens.json | 10 +++++++++ .../core/ui/extensions/BlockchainIcons.kt | 5 +++++ core/ui/src/main/res/drawable/ic_base_22.xml | 18 +++++++++++++++ core/ui/src/main/res/drawable/img_base_22.xml | 16 ++++++++++++++ .../QuotesUnsupportedCurrenciesIdAdapter.kt | 1 + .../domain/common/extensions/Blockchain.kt | 5 +++++ .../DefaultWalletManagersFacade.kt | 22 +++++++++++++++++-- gradle/dependencies.toml | 4 ++-- 8 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_base_22.xml create mode 100644 core/ui/src/main/res/drawable/img_base_22.xml diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index 423202eeda..e10692c072 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -678,6 +678,16 @@ "networkId": "polygon-zkevm/test" } ] + }, + { + "id": "base-ethereum", + "name": "Base", + "symbol": "ETH", + "networks": [ + { + "networkId": "base/test" + } + ] } ] } diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 0a77e69e7e..1a95df0142 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -66,6 +66,7 @@ fun getActiveIconRes(blockchainId: String): Int { "flare", "flare/test" -> R.drawable.img_flare_22 "taraxa", "taraxa/test" -> R.drawable.img_taraxa_22 "radiant" -> R.drawable.img_radiant_22 + "base" -> R.drawable.img_base_22 else -> R.drawable.ic_alert_24 } } @@ -133,6 +134,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "flare-network", "flare-network/test" -> R.drawable.img_flare_22 "taraxa", "taraxa/test" -> R.drawable.img_taraxa_22 "radiant" -> R.drawable.img_radiant_22 + "base" -> R.drawable.img_base_22 else -> R.drawable.ic_alert_24 } } @@ -197,6 +199,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "flare-networks" -> R.drawable.img_flare_22 "taraxa" -> R.drawable.img_taraxa_22 "radiant" -> R.drawable.img_radiant_22 + "base" -> R.drawable.img_base_22 else -> R.drawable.ic_alert_24 } } @@ -264,6 +267,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "flare", "flare/test" -> R.drawable.ic_flare_22 "taraxa", "taraxa/test" -> R.drawable.ic_taraxa_22 "radiant" -> R.drawable.ic_radiant_22 + "base", "base/test" -> R.drawable.ic_base_22 else -> R.drawable.ic_alert_24 } } @@ -331,6 +335,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "flare-network", "flare-network/test" -> R.drawable.ic_flare_22 "taraxa", "taraxa/test" -> R.drawable.ic_taraxa_22 "radiant" -> R.drawable.ic_radiant_22 + "base", "base/test" -> R.drawable.ic_base_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_base_22.xml b/core/ui/src/main/res/drawable/ic_base_22.xml new file mode 100644 index 0000000000..7c135649e4 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_base_22.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_base_22.xml b/core/ui/src/main/res/drawable/img_base_22.xml new file mode 100644 index 0000000000..a426d0c8b2 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_base_22.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt index 6b2779299c..0e69a31330 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt @@ -55,6 +55,7 @@ internal class QuotesUnsupportedCurrenciesIdAdapter { "manta-network-ethereum" to "ethereum", "polygon-zkevm-ethereum" to "ethereum", "aurora-ethereum" to "ethereum", + "base-ethereum" to "ethereum", ) } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index e3ed6aabed..d3a65d7373 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -112,6 +112,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "flare-network/test" -> Blockchain.FlareTestnet "taraxa" -> Blockchain.Taraxa "taraxa/test" -> Blockchain.TaraxaTestnet + "base" -> Blockchain.Base + "base/test" -> Blockchain.BaseTestnet else -> null } } @@ -225,6 +227,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.FlareTestnet -> "flare-network/test" Blockchain.Taraxa -> "taraxa" Blockchain.TaraxaTestnet -> "taraxa/test" + Blockchain.Base -> "base" + Blockchain.BaseTestnet -> "base/test" } } @@ -297,6 +301,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Mantle, Blockchain.MantleTestnet -> "mantle" Blockchain.Flare, Blockchain.FlareTestnet -> "flare-networks" Blockchain.Taraxa, Blockchain.TaraxaTestnet -> "taraxa" + Blockchain.Base, Blockchain.BaseTestnet -> "base" } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 276758748b..951d8ff4ef 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -192,7 +192,16 @@ class DefaultWalletManagersFacade( address = walletManager.wallet.address, filterType = when (currency) { is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin - is CryptoCurrency.Token -> TransactionHistoryRequest.FilterType.Contract(currency.contractAddress) + is CryptoCurrency.Token -> { + val blockchainToken = Token( + name = currency.name, + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimals, + id = currency.id.rawCurrencyId, + ) + TransactionHistoryRequest.FilterType.Contract(blockchainToken) + } }, ) .let(txHistoryStateConverter::convert) @@ -221,7 +230,16 @@ class DefaultWalletManagersFacade( pageSize = pageSize, filterType = when (currency) { is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin - is CryptoCurrency.Token -> TransactionHistoryRequest.FilterType.Contract(currency.contractAddress) + is CryptoCurrency.Token -> { + val blockchainToken = Token( + name = currency.name, + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimals, + id = currency.id.rawCurrencyId, + ) + TransactionHistoryRequest.FilterType.Contract(blockchainToken) + } }, ), ) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index d02fccec51..4995c29d6a 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,9 +85,9 @@ leakcanary = "2.13" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.9-580" +tangemBlockchainSdk = "release-app_5.9-582" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.9-340" +tangemCardSdk = "release-app_5.9-343" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem From 6d325ab00804816e53f12c41da1f41c5f35e53b6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Apr 2024 19:45:15 +0100 Subject: [PATCH 084/109] Updated on 2026-08-14 --- .../DefaultWalletManagersFacade.kt | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 951d8ff4ef..276758748b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -192,16 +192,7 @@ class DefaultWalletManagersFacade( address = walletManager.wallet.address, filterType = when (currency) { is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin - is CryptoCurrency.Token -> { - val blockchainToken = Token( - name = currency.name, - symbol = currency.symbol, - contractAddress = currency.contractAddress, - decimals = currency.decimals, - id = currency.id.rawCurrencyId, - ) - TransactionHistoryRequest.FilterType.Contract(blockchainToken) - } + is CryptoCurrency.Token -> TransactionHistoryRequest.FilterType.Contract(currency.contractAddress) }, ) .let(txHistoryStateConverter::convert) @@ -230,16 +221,7 @@ class DefaultWalletManagersFacade( pageSize = pageSize, filterType = when (currency) { is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin - is CryptoCurrency.Token -> { - val blockchainToken = Token( - name = currency.name, - symbol = currency.symbol, - contractAddress = currency.contractAddress, - decimals = currency.decimals, - id = currency.id.rawCurrencyId, - ) - TransactionHistoryRequest.FilterType.Contract(blockchainToken) - } + is CryptoCurrency.Token -> TransactionHistoryRequest.FilterType.Contract(currency.contractAddress) }, ), ) From 06fae63efa7e54f3f8e3f9a43006ae6b98968d33 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Apr 2024 11:53:17 +0500 Subject: [PATCH 085/109] Updated on 2026-08-14 --- .../ui/components/fields/SimpleTextField.kt | 17 +++++++++++++---- .../state/confirm/SendNotificationFactory.kt | 8 +++----- .../presentation/viewmodel/SendViewModel.kt | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt index 6d31ebeaef..ed91b52667 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -71,7 +71,18 @@ fun SimpleTextField( } } - var lastTextValue by remember(value) { mutableStateOf(value) } + var lastTextValue by remember(value) { + derivedStateOf { + val isSelectionLastIndex = textFieldValueState.selection.end == textFieldValueState.text.lastIndex + if (textFieldValueState.text.isBlank() || isSelectionLastIndex) { + textFieldValueState = textFieldValueState.copy( + text = value, + selection = getValueRange(value), + ) + } + } + mutableStateOf(value) + } CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) { BasicTextField( @@ -82,9 +93,7 @@ fun SimpleTextField( val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text lastTextValue = newTextFieldValueState.text - if (stringChangedSinceLastInvocation) { - onValueChange(newTextFieldValueState.text) - } + if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text) }, textStyle = textStyle.copy(color = color), cursorBrush = SolidColor(TangemTheme.colors.text.primary1), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 8b5e5530c2..782b8beb8e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -17,10 +17,6 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents import com.tangem.features.send.impl.presentation.state.* -import com.tangem.features.send.impl.presentation.state.SendNotification -import com.tangem.features.send.impl.presentation.state.SendStates -import com.tangem.features.send.impl.presentation.state.SendUiState -import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents @@ -38,6 +34,7 @@ import java.math.BigDecimal internal class SendNotificationFactory( private val cryptoCurrencyStatusProvider: Provider, private val coinCryptoCurrencyStatusProvider: Provider, + private val feePaidCryptoCurrencyStatusProvider: Provider, private val currentStateProvider: Provider, private val userWalletProvider: Provider, private val currencyChecksRepository: CurrencyChecksRepository, @@ -91,10 +88,11 @@ internal class SendNotificationFactory( ) { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider() + val feePaidCryptoCurrencyStatus = feePaidCryptoCurrencyStatusProvider() val cryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO val coinCryptoAmount = coinCryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO - val showNotification = if (cryptoCurrencyStatus.currency is CryptoCurrency.Token) { + val showNotification = if (cryptoCurrencyStatus.currency.id == feePaidCryptoCurrencyStatus?.currency?.id) { receivedAmount > cryptoAmount || feeAmount > coinCryptoAmount } else { receivedAmount + feeAmount > cryptoAmount diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 069a34f6a3..99e752dd9f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -155,6 +155,7 @@ internal class SendViewModel @Inject constructor( private val sendNotificationFactory = SendNotificationFactory( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, + feePaidCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, currentStateProvider = Provider { uiState }, userWalletProvider = Provider { userWallet }, stateRouterProvider = Provider { stateRouter }, From 505400888ddd1178e6d3daa917b5a0f5118ae008 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Apr 2024 10:10:09 +0100 Subject: [PATCH 086/109] Updated on 2026-08-14 --- .../main/java/com/tangem/domain/common/extensions/Blockchain.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index d3a65d7373..ff110169a6 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -301,7 +301,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Mantle, Blockchain.MantleTestnet -> "mantle" Blockchain.Flare, Blockchain.FlareTestnet -> "flare-networks" Blockchain.Taraxa, Blockchain.TaraxaTestnet -> "taraxa" - Blockchain.Base, Blockchain.BaseTestnet -> "base" + Blockchain.Base, Blockchain.BaseTestnet -> "base-ethereum" } } From 230aff6ed66c8fcc9fcbb845ff08f2261a218c19 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Apr 2024 16:09:32 +0500 Subject: [PATCH 087/109] Updated on 2026-08-14 --- .../transaction/error/SendTransactionError.kt | 2 +- .../usecase/SendTransactionUseCase.kt | 7 ++- .../state/SendTransactionAlertConverter.kt | 4 +- .../state/confirm/SendNotificationFactory.kt | 45 +++++++++---------- .../com/tangem/lib/crypto/BlockchainUtils.kt | 5 +++ 5 files changed, 34 insertions(+), 29 deletions(-) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt index 753f45e4b4..22b7991290 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt @@ -8,7 +8,7 @@ sealed class SendTransactionError { data class DataError(val message: String?) : SendTransactionError() - data class NetworkError(val message: String?) : SendTransactionError() + data class NetworkError(val message: String?, val code: String?) : SendTransactionError() data class BlockchainSdkError(val code: Int, val message: String?) : SendTransactionError() diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 059a672c70..d2c9cbd085 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -67,7 +67,12 @@ class SendTransactionUseCase( } private fun handleError(result: SimpleResult.Failure): SendTransactionError { - if (ResultChecker.isNetworkError(result)) return SendTransactionError.NetworkError(result.error.message) + if (ResultChecker.isNetworkError(result)) { + return SendTransactionError.NetworkError( + code = result.error.message, + message = result.error.customMessage, + ) + } val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError() return when (error) { is BlockchainSdkError.WrappedTangemError -> { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt index a35b421a16..ad5d16e175 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt @@ -29,8 +29,8 @@ internal class SendTransactionAlertConverter( onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) }, ) is SendTransactionError.NetworkError -> SendAlertState.TransactionError( - code = "", - cause = value.message, + code = value.code.orEmpty(), + cause = value.message.orEmpty(), onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) }, ) is SendTransactionError.UnknownError -> SendAlertState.TransactionError( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 782b8beb8e..709779d1df 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -20,6 +20,7 @@ import com.tangem.features.send.impl.presentation.state.* import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.lib.crypto.BlockchainUtils.isDogecoin import com.tangem.utils.Provider import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.ImmutableList @@ -52,18 +53,17 @@ internal class SendNotificationFactory( val feeState = state.feeState ?: return@map persistentListOf() val feeAmount = feeState.fee?.amount?.value ?: BigDecimal.ZERO val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO - val sendAmount = if (sendState.isSubtract) amountValue.minus(feeAmount) else amountValue buildList { // errors - addExceedBalanceNotification(feeAmount, sendAmount) + addExceedBalanceNotification(feeAmount, amountValue) addExceedsBalanceNotification(feeState.fee) - addInvalidAmountNotification(sendState.isSubtract, sendAmount) - addMinimumAmountErrorNotification(feeAmount, sendAmount) - addDustWarningNotification(feeAmount, sendAmount) - addTransactionLimitErrorNotification(feeAmount, sendAmount) + addInvalidAmountNotification(sendState.isSubtract, amountValue) + addMinimumAmountErrorNotification(feeAmount, amountValue) + addDustWarningNotification(feeAmount, amountValue) + addTransactionLimitErrorNotification(feeAmount, amountValue) // warnings - addExistentialWarningNotification(feeAmount, sendAmount) - addHighFeeWarningNotification(sendAmount, sendState.ignoreAmountReduce) + addExistentialWarningNotification(feeAmount, amountValue) + addHighFeeWarningNotification(feeAmount, amountValue, sendState.ignoreAmountReduce) addTooLowNotification(feeState) }.toImmutableList() } @@ -121,20 +121,11 @@ internal class SendNotificationFactory( val totalAmount = feeAmount + receivedAmount val balance = coinCryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO - // TODO Move Blockchain check elsewhere - when (coinCryptoCurrencyStatus.currency.network.id.value) { - Blockchain.Cardano.id -> { - if (receivedAmount > BigDecimal.ONE || balance - totalAmount < BigDecimal.ONE) { - add(SendNotification.Error.MinimumAmountError(CARDANO_MINIMUM)) - } + if (isDogecoin(coinCryptoCurrencyStatus.currency.network.id.value)) { + val minimum = BigDecimal(DOGECOIN_MINIMUM) + if (receivedAmount < minimum || balance - totalAmount < minimum) { + add(SendNotification.Error.MinimumAmountError(DOGECOIN_MINIMUM)) } - Blockchain.Dogecoin.id -> { - val minimum = BigDecimal(DOGECOIN_MINIMUM) - if (receivedAmount > minimum || balance - totalAmount < minimum) { - add(SendNotification.Error.MinimumAmountError(DOGECOIN_MINIMUM)) - } - } - else -> Unit } } @@ -199,7 +190,9 @@ internal class SendNotificationFactory( receivedAmount: BigDecimal, ) { val userWalletId = userWalletProvider().walletId - val cryptoCurrency = cryptoCurrencyStatusProvider().currency + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + val balance = cryptoCurrencyStatus.value.amount ?: return val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) { feeAmount } else { @@ -209,7 +202,8 @@ internal class SendNotificationFactory( userWalletId, cryptoCurrency.network, ) - if (currencyDeposit != null && currencyDeposit > spendingAmount) { + val diff = balance.minus(spendingAmount) + if (currencyDeposit != null && currencyDeposit > diff) { add( SendNotification.Error.ExistentialDeposit( BigDecimalFormatter.formatCryptoAmount( @@ -222,13 +216,15 @@ internal class SendNotificationFactory( } private fun MutableList.addHighFeeWarningNotification( + feeAmount: BigDecimal, sendAmount: BigDecimal, ignoreAmountReduce: Boolean, ) { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO val isTezos = cryptoCurrencyStatus.currency.network.id.value == Blockchain.Tezos.id - if (!ignoreAmountReduce && sendAmount == balance && isTezos) { + val isTotalBalance = feeAmount.plus(sendAmount) >= balance + if (!ignoreAmountReduce && isTotalBalance && isTezos) { add( SendNotification.Warning.HighFeeError( amount = TEZOS_FEE_THRESHOLD.toPlainString(), @@ -361,7 +357,6 @@ internal class SendNotificationFactory( } companion object { - private const val CARDANO_MINIMUM = "1" private const val DOGECOIN_MINIMUM = "0.01" private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01") } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index a6e7acf14d..cf10f1ce54 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -32,4 +32,9 @@ object BlockchainUtils { val blockchain = Blockchain.fromId(networkId) return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet } + + fun isDogecoin(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Dogecoin + } } \ No newline at end of file From dd9ad45d0937c431d99ca7d162439468e9e9d525 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Apr 2024 14:37:24 +0100 Subject: [PATCH 088/109] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../java/com/tangem/datasource/config/ConfigManagerImpl.kt | 3 +++ .../java/com/tangem/datasource/config/models/JsonModels.kt | 3 +++ .../java/com/tangem/domain/common/extensions/Blockchain.kt | 1 - gradle/dependencies.toml | 2 +- 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 82f4a8aebc..980cc4cb2f 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 82f4a8aebcab924047705ad3ecd5f008f07ca0b5 +Subproject commit 980cc4cb2fb9ba44cd6798191ae7d208e4f7f74c diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index 8ebac44c3b..d26cc5e36e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -146,6 +146,9 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { blockBookRest = accessTokens.bitcoin?.blockBookRest, ), algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest), + zkSync = GetBlockAccessToken(rest = accessTokens.zksync?.rest), + polygonZkevm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.rest), + base = GetBlockAccessToken(rest = accessTokens.base?.rest), ) } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index 0e572bd3e5..94987bf391 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -67,6 +67,9 @@ data class GetBlockAccessTokens( @Json(name = "dash") val dash: GetBlockToken?, @Json(name = "bitcoin") val bitcoin: GetBlockToken?, @Json(name = "algorand") val algorand: GetBlockToken?, + @Json(name = "polygon-zkevm") val polygonZkevm: GetBlockToken?, + @Json(name = "zksync") val zksync: GetBlockToken?, + @Json(name = "base") val base: GetBlockToken?, ) @JsonClass(generateAdapter = true) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index ff110169a6..36caa85c7f 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -328,7 +328,6 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 private val excludedBlockchains = listOf( Blockchain.Unknown, - Blockchain.Playa3ull, Blockchain.Nexa, Blockchain.NexaTestnet, Blockchain.Radiant, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 4995c29d6a..8fe2844437 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,7 +85,7 @@ leakcanary = "2.13" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.9-582" +tangemBlockchainSdk = "release-app_5.9-589" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.9-343" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 8735392aff769462a08c3a42c44a19d1d2084d35 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Apr 2024 18:01:31 +0500 Subject: [PATCH 089/109] Updated on 2026-08-14 --- .../send/impl/presentation/ui/SendScreen.kt | 2 +- .../ui/fee/SendSpeedSelectorItem.kt | 34 ++----------------- .../impl/presentation/ui/send/FeeBlock.kt | 18 +++------- .../impl/presentation/utils/FormatterUtils.kt | 34 +++++++++++++++++++ 4 files changed, 41 insertions(+), 47 deletions(-) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 55bebe81a7..2dcff9830c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -33,6 +33,7 @@ internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) Column( modifier = Modifier .fillMaxSize() + .imePadding() .systemBarsPadding() .background(color = TangemTheme.colors.background.tertiary), horizontalAlignment = Alignment.CenterHorizontally, @@ -78,7 +79,6 @@ internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) SendNavigationButtons( uiState = uiState, currentState = currentState, - modifier = Modifier.imePadding(), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index 3ecc6ddf2e..bb360d218b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -17,18 +17,15 @@ import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.rows.SelectorRowItem -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType -import java.math.BigDecimal +import com.tangem.features.send.impl.presentation.utils.getCryptoReference +import com.tangem.features.send.impl.presentation.utils.getFiatReference @Composable internal fun SendSpeedSelectorItem( @@ -76,33 +73,6 @@ internal fun SendSpeedSelectorItem( } } -// todo remove after refactoring [REDACTED_JIRA] -private fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { - if (amount == null) return null - return combinedReference( - if (isFeeApproximate) stringReference("${BigDecimalFormatter.CAN_BE_LOWER_SIGN} ") else TextReference.EMPTY, - stringReference( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = amount.value, - cryptoCurrency = amount.currencySymbol, - decimals = amount.decimals, - ), - ), - ) -} - -// todo remove after refactoring [REDACTED_JIRA] -private fun getFiatReference(amount: Amount?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { - if (amount == null) return null - return stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = rate?.let { amount.value?.multiply(it) }, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), - ) -} - @Composable private fun SendSpeedSelectorItemError(isError: Boolean) { Row { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index 23af8a4893..a3ee42ccbb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -15,14 +15,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.rows.SelectorRowItem -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.previewdata.FeeStatePreviewData +import com.tangem.features.send.impl.presentation.utils.getCryptoReference +import com.tangem.features.send.impl.presentation.utils.getFiatReference @Composable internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) { @@ -34,16 +34,6 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick FeeType.Fast -> R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24 FeeType.Custom -> R.string.common_fee_selector_option_custom to R.drawable.ic_edit_24 } - val feeCryptoValue = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = fee.amount.value, - cryptoCurrency = fee.amount.currencySymbol, - decimals = fee.amount.decimals, - ) - val feeFiatValue = BigDecimalFormatter.formatFiatAmount( - fiatAmount = feeState.rate?.let { fee.amount.value?.multiply(it) }, - fiatCurrencyCode = feeState.appCurrency.code, - fiatCurrencySymbol = feeState.appCurrency.symbol, - ) Column( modifier = Modifier .fillMaxWidth() @@ -60,8 +50,8 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick SelectorRowItem( titleRes = title, iconRes = icon, - preDot = stringReference(feeCryptoValue), - postDot = stringReference(feeFiatValue), + preDot = getCryptoReference(fee.amount, feeState.isFeeApproximate), + postDot = getFiatReference(fee.amount, feeState.rate, feeState.appCurrency), ellipsizeOffset = fee.amount.currencySymbol.length, isSelected = true, showDivider = false, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt new file mode 100644 index 0000000000..022d0c5711 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt @@ -0,0 +1,34 @@ +package com.tangem.features.send.impl.presentation.utils + +import com.tangem.blockchain.common.Amount +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import java.math.BigDecimal + +internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { + if (amount == null) return null + return combinedReference( + if (isFeeApproximate) stringReference("${BigDecimalFormatter.CAN_BE_LOWER_SIGN} ") else TextReference.EMPTY, + stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = amount.value, + cryptoCurrency = amount.currencySymbol, + decimals = amount.decimals, + ), + ), + ) +} + +internal fun getFiatReference(amount: Amount?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { + if (amount == null) return null + return stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = rate?.let { amount.value?.multiply(it) }, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ) +} \ No newline at end of file From badcca6fb9c147ad5820f77e0f5d2c7f0b6051b4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Apr 2024 18:02:08 +0500 Subject: [PATCH 090/109] Updated on 2026-08-14 --- .../fee/custom/BitcoinCustomFeeConverter.kt | 70 ++++++++++--------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt index 6fb6ce20ef..e42f25a9d5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt @@ -17,6 +17,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -32,40 +33,45 @@ internal class BitcoinCustomFeeConverter( override fun convert(value: Fee.Bitcoin): ImmutableList { val feeValue = value.amount.value - return persistentListOf( - SendTextField.CustomFee( - value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(), - decimals = value.amount.decimals, - symbol = value.amount.currencySymbol, - onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) }, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Number, - ), - title = resourceReference(R.string.send_max_fee), - footer = resourceReference(R.string.send_max_fee_footer), - label = getFeeFormatted(feeValue), - keyboardActions = KeyboardActions(), - isReadonly = true, - ), - SendTextField.CustomFee( - value = toSatoshiPerByte( - amount = feeValue, + val network = feeCryptoCurrencyStatusProvider()?.currency?.network?.id?.value + return if (network != null && isBitcoin(network)) { + persistentListOf( + SendTextField.CustomFee( + value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(), decimals = value.amount.decimals, - txSize = value.txSize, - ).toString(), - decimals = SATOSHI_DECIMALS, - symbol = "", - title = resourceReference(R.string.send_gas_price), - footer = resourceReference(R.string.send_gas_price_footer), - onValueChange = { clickIntents.onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) }, - keyboardOptions = KeyboardOptions( - imeAction = if (checkExceedBalance(feeValue)) ImeAction.None else ImeAction.Done, - keyboardType = KeyboardType.Number, + symbol = value.amount.currencySymbol, + onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + title = resourceReference(R.string.send_max_fee), + footer = resourceReference(R.string.send_max_fee_footer), + label = getFeeFormatted(feeValue), + keyboardActions = KeyboardActions(), + isReadonly = true, ), - keyboardActions = KeyboardActions(), - ), - ) + SendTextField.CustomFee( + value = toSatoshiPerByte( + amount = feeValue, + decimals = value.amount.decimals, + txSize = value.txSize, + ).toString(), + decimals = SATOSHI_DECIMALS, + symbol = "", + title = resourceReference(R.string.send_gas_price), + footer = resourceReference(R.string.send_gas_price_footer), + onValueChange = { clickIntents.onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) }, + keyboardOptions = KeyboardOptions( + imeAction = if (checkExceedBalance(feeValue)) ImeAction.None else ImeAction.Done, + keyboardType = KeyboardType.Number, + ), + keyboardActions = KeyboardActions(), + ), + ) + } else { + persistentListOf() + } } override fun convertBack(normalFee: Fee.Bitcoin, value: ImmutableList): Fee.Bitcoin { From 1264ada90d1f8dfe96c1c848c17a928826e5b83f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Apr 2024 18:02:49 +0500 Subject: [PATCH 091/109] Updated on 2026-08-14 --- .../impl/presentation/state/SendEventStateFactory.kt | 7 ++++++- .../state/SendTransactionAlertConverter.kt | 11 ++++++++++- .../send/impl/presentation/viewmodel/SendViewModel.kt | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt index 58e0a97b65..3e615c7835 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory @@ -21,11 +22,15 @@ import java.math.BigDecimal */ internal class SendEventStateFactory( private val currentStateProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, private val clickIntents: SendClickIntents, private val feeStateFactory: FeeStateFactory, ) { private val sendTransactionErrorConverter by lazy(LazyThreadSafetyMode.NONE) { - SendTransactionAlertConverter(clickIntents) + SendTransactionAlertConverter( + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + clickIntents = clickIntents, + ) } fun onConsumeEventState(): SendUiState { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt index ad5d16e175..7f6a8588b1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt @@ -1,10 +1,14 @@ package com.tangem.features.send.impl.presentation.state +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider import com.tangem.utils.converter.Converter internal class SendTransactionAlertConverter( + private val cryptoCurrencyStatusProvider: Provider, private val clickIntents: SendClickIntents, ) : Converter { override fun convert(value: SendTransactionError): SendAlertState? { @@ -38,7 +42,12 @@ internal class SendTransactionAlertConverter( cause = value.ex?.localizedMessage, onConfirmClick = { clickIntents.onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) }, ) - is SendTransactionError.CreateAccountUnderfunded -> SendAlertState.ReserveAmount(value.amount) + is SendTransactionError.CreateAccountUnderfunded -> SendAlertState.ReserveAmount( + BigDecimalFormatter.formatWithSymbol( + amount = value.amount, + symbol = cryptoCurrencyStatusProvider().currency.symbol, + ), + ) else -> null } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 99e752dd9f..1533c95e7c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -143,6 +143,7 @@ internal class SendViewModel @Inject constructor( private val eventStateFactory = SendEventStateFactory( clickIntents = this, currentStateProvider = Provider { uiState }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, feeStateFactory = feeStateFactory, ) @@ -784,7 +785,6 @@ internal class SendViewModel @Inject constructor( override fun onAmountReduceClick(reducedAmount: String, clazz: Class) { uiState = amountStateFactory.getOnAmountValueChange(reducedAmount) uiState = sendNotificationFactory.dismissNotificationState(clazz) - onCheckFeeUpdate() } override fun onNotificationCancel(clazz: Class) { From 08ddee9accccfb85a27832cbdac65a065bc44f5e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Apr 2024 19:59:05 +0500 Subject: [PATCH 092/109] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 10 ++--- core/res/src/main/res/values/strings.xml | 4 +- .../impl/presentation/state/SendAlertState.kt | 10 +++++ .../state/SendEventStateFactory.kt | 14 +++++++ .../state/confirm/SendNotificationFactory.kt | 15 ++++++++ .../presentation/state/fee/FeeCalculation.kt | 16 ++++++++ .../state/fee/FeeNotificationFactory.kt | 38 +------------------ .../ui/fee/SendSpeedSelectorItem.kt | 32 ---------------- .../presentation/viewmodel/SendViewModel.kt | 10 ++++- 9 files changed, 73 insertions(+), 76 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 136cc2857f..256b94877c 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -79,7 +79,7 @@ Посмотреть историю транзакций Обозреватель Комиссия - Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s + Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s Свое Быстро По рынку @@ -310,7 +310,7 @@ В этом случае вам будет необходимо начать процесс заново. Вы хотите выйти из процесса активации? Подготовка - Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа? + Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Если на нем есть средства, пожалуйста сначала выведите их, а затем сделайте сброс до заводских настроек и используйте как резервную. Резервная копия Прочитать о seed-фразе @@ -426,12 +426,12 @@ Приготовьте свою карту Уже содержится в введенном адресе Вычесть - Недостаточно средств для покрытия комиссии сети. Вычесть комиссию %s из отправляемой сумму? + Недостаточно средств для покрытия комиссии сети. Вычесть комиссию %s из отправляемой суммы? + Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? Причина: %1$s\nКод: %2$s Транзакция не выполнена Сумма - Подтверждение %1$s, %2$s Адрес Код назначения @@ -536,7 +536,7 @@ Балансы скрыты Балансы показаны Отменить - В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением.” + В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением. У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. Выбранная операция в данный момент недоступна. Попробуйте позже. Обмен %s не доступен. Но мы работаем над его добавлением. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index eacae3a8ac..a68246f965 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -310,7 +310,7 @@ In this case, you will need to start from the beginning. Do you want to exit the activation process? Getting started - Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet? + Another wallet has already been created on the card you\'re trying to add. If you have funds in this wallet, please withdraw it and then reset this card and add it as a backup. Creating a backup Read more about seed phrase @@ -422,11 +422,11 @@ Already included in the entered address Subtract Not enough funds to cover the network commission. Subtract the commission %s from the amount sent? + The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue? Reason: %1$s\nCode: %2$s The transaction is not completed Amount - Confirm %1$s, %2$s Address Destination Tag diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt index d3adc2e74c..b8da006bb9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt @@ -58,6 +58,16 @@ internal sealed class SendAlertState { override val confirmButtonText: TextReference = resourceReference(R.string.common_continue) } + data class FeeTooHigh( + val times: String, + override val onConfirmClick: () -> Unit, + ) : SendAlertState() { + override val title: TextReference? = null + override val message: TextReference = + resourceReference(id = R.string.send_alert_fee_too_high_text, wrappedList(times)) + override val confirmButtonText: TextReference = resourceReference(R.string.common_continue) + } + data class FeeCoverage( val amount: String, override val onConfirmClick: (() -> Unit), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt index 3e615c7835..1798d8fe79 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt @@ -115,6 +115,20 @@ internal class SendEventStateFactory( ) } + fun getFeeTooHighAlert(diff: String, onConsume: () -> Unit): SendUiState { + return currentStateProvider().copy( + event = triggeredEvent( + data = SendEvent.ShowAlert( + SendAlertState.FeeTooHigh( + onConfirmClick = clickIntents::showSend, + times = diff, + ), + ), + onConsume = onConsume, + ), + ) + } + fun getGenericErrorState(error: Throwable? = null, onConsume: () -> Unit): SendUiState { val state = currentStateProvider() return state.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 709779d1df..6a4e8a383e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import java.math.BigDecimal +import java.math.BigInteger @Suppress("LongParameterList") internal class SendNotificationFactory( @@ -64,6 +65,7 @@ internal class SendNotificationFactory( // warnings addExistentialWarningNotification(feeAmount, amountValue) addHighFeeWarningNotification(feeAmount, amountValue, sendState.ignoreAmountReduce) + addTooHighNotification(feeState.feeSelectorState) addTooLowNotification(feeState) }.toImmutableList() } @@ -279,6 +281,18 @@ internal class SendNotificationFactory( } } + private fun MutableList.addTooHighNotification(feeSelectorState: FeeSelectorState) { + if (feeSelectorState !is FeeSelectorState.Content) return + val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return + val highValue = multipleFees.priority.amount.value ?: return + val customAmount = feeSelectorState.customValues.firstOrNull() ?: return + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) + val diff = (customValue / highValue).toBigInteger() + if (feeSelectorState.selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF) { + add(SendNotification.Warning.TooHigh(diff.toString())) + } + } + private suspend fun MutableList.addExceedsBalanceNotification(fee: Fee?) { val feeValue = fee?.amount?.value ?: BigDecimal.ZERO val userWalletId = userWalletProvider().walletId @@ -359,5 +373,6 @@ internal class SendNotificationFactory( companion object { private const val DOGECOIN_MINIMUM = "0.01" private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01") + internal val FEE_MAX_DIFF = BigInteger("5") } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt index 1a57669334..06a47de259 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory /** * Check if sending amount with fee is greater than balance @@ -26,4 +27,19 @@ internal fun checkIfFeeTooLow(state: SendUiState): Boolean { val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) return feeSelectorState.selectedFee == FeeType.Custom && minimumValue > customValue +} + +/** + * Check if custom fee is too high + */ +internal fun checkIfFeeTooHigh(state: SendUiState, onShow: (String) -> Unit): Boolean { + val feeSelectorState = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false + val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return false + val highValue = multipleFees.priority.amount.value ?: return false + val customAmount = feeSelectorState.customValues.firstOrNull() ?: return false + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) + val diff = (customValue / highValue).toBigInteger() + val isShow = feeSelectorState.selectedFee == FeeType.Custom && diff > SendNotificationFactory.FEE_MAX_DIFF + if (isShow) onShow(diff.toString()) + return isShow } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt index 217631c38e..92189a24ef 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt @@ -1,20 +1,15 @@ package com.tangem.features.send.impl.presentation.state.fee -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.state.StateRouter -import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider -import com.tangem.utils.toFormattedString import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map -import java.math.BigDecimal @Suppress("LongParameterList") internal class FeeNotificationFactory( @@ -29,16 +24,7 @@ internal class FeeNotificationFactory( val state = currentStateProvider() val feeState = state.feeState ?: return@map persistentListOf() buildList { - when (val feeSelectorState = feeState.feeSelectorState) { - FeeSelectorState.Error -> { - addFeeUnreachableNotification(feeSelectorState) - } - is FeeSelectorState.Content -> { - val customFee = feeSelectorState.customValues - val selectedFee = feeSelectorState.selectedFee - addTooHighNotification(feeSelectorState.fees, selectedFee, customFee) - } - } + addFeeUnreachableNotification(feeState.feeSelectorState) }.toImmutableList() } @@ -47,24 +33,4 @@ internal class FeeNotificationFactory( add(SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload)) } } - - private fun MutableList.addTooHighNotification( - transactionFee: TransactionFee, - selectedFee: FeeType, - customFee: List, - ) { - val multipleFees = transactionFee as? TransactionFee.Choosable ?: return - val highValue = multipleFees.priority.amount.value ?: return - val customAmount = customFee.firstOrNull() ?: return - val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) - val diff = customValue / highValue - if (selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF) { - add(SendNotification.Warning.TooHigh(diff.toFormattedString(HIGH_FEE_DIFF_DECIMALS))) - } - } - - companion object { - private val FEE_MAX_DIFF = BigDecimal(5) - private const val HIGH_FEE_DIFF_DECIMALS = 0 - } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index bb360d218b..0a2a65c93e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -3,7 +3,6 @@ package com.tangem.features.send.impl.presentation.ui.fee import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.* -import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -12,15 +11,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType @@ -64,11 +60,6 @@ internal fun SendSpeedSelectorItem( showDivider = showDivider, ) SendSpeedSelectorItemError(isError = feeSelectorState is FeeSelectorState.Error) - - if (feeType == FeeType.Custom) { - val showWarning = state.notifications.any { it is SendNotification.Warning.TooHigh } - WarningIcon(showWarning = showWarning) - } } } } @@ -97,29 +88,6 @@ private fun SendSpeedSelectorItemError(isError: Boolean) { } } -@Composable -private fun WarningIcon(showWarning: Boolean = false) { - Row { - SpacerWMax() - AnimatedVisibility( - visible = showWarning, - label = "Custom fee warning indicator", - enter = fadeIn(), - exit = fadeOut(), - ) { - Image( - painter = painterResource(R.drawable.ic_alert_triangle_20), - contentDescription = null, - modifier = Modifier - .padding( - vertical = TangemTheme.dimens.spacing12, - horizontal = TangemTheme.dimens.spacing14, - ), - ) - } - } -} - private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? { val choosableFees = fees as? TransactionFee.Choosable return when (feeType) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 1533c95e7c..8564920606 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -525,7 +525,15 @@ internal class SendViewModel @Inject constructor( ) return true } - return false + return checkIfFeeTooHigh( + state = uiState, + onShow = { diff -> + uiState = eventStateFactory.getFeeTooHighAlert( + diff = diff, + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + }, + ) } private fun onFeeCoverageAlert(): Boolean { From ff486475ea2071a21b8f79948147af4e3318fe74 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Apr 2024 12:48:55 +0500 Subject: [PATCH 093/109] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 5 +++-- core/res/src/main/res/values/strings.xml | 6 ++++-- .../send/impl/presentation/state/SendAlertState.kt | 3 +-- .../impl/presentation/state/SendEventStateFactory.kt | 11 +---------- .../send/impl/presentation/state/SendUiState.kt | 1 + .../state/amount/SendAmountStateConverter.kt | 1 + .../state/amount/SendAmountSubtractConverter.kt | 7 +++++-- .../impl/presentation/state/fee/FeeCalculation.kt | 2 +- .../state/fee/custom/BitcoinCustomFeeConverter.kt | 4 ++-- .../state/fields/SendAmountFieldChangeConverter.kt | 1 + .../state/fields/SendAmountFieldMaxAmountConverter.kt | 1 + .../state/previewdata/AmountStatePreviewData.kt | 1 + 12 files changed, 22 insertions(+), 21 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 256b94877c..b4bd309592 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -426,8 +426,8 @@ Приготовьте свою карту Уже содержится в введенном адресе Вычесть - Недостаточно средств для покрытия комиссии сети. Вычесть комиссию %s из отправляемой суммы? - Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. + Недостаточно средств для покрытия комиссии сети. Вычесть недостающую сумму для покрытия комиссии из отправляемой суммы? + Сумма комиссии в %@ раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? Причина: %1$s\nКод: %2$s Транзакция не выполнена @@ -487,6 +487,7 @@ Отправить Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. Мои кошельки + Это способ измерения комиссии за отправку биткоин-транзакции. Он указывает на количество самой маленькой единицы биткоина (сатоши) за каждый байт данных в транзакции. Чем выше это число, тем быстрее будет обработана транзакция сетью. Отправка Нажмите на любое поле, чтобы изменить его Отправка %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index a68246f965..a92316eb37 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -421,8 +421,8 @@ Get your card ready! Already included in the entered address Subtract - Not enough funds to cover the network commission. Subtract the commission %s from the amount sent? - The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. + Not enough funds to cover the network fee. Do you want to subtract the amount required to cover the fee? + The commission amount is %@ times the recommended amount. Make sure that the custom settings are correct. You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue? Reason: %1$s\nCode: %2$s The transaction is not completed @@ -482,6 +482,8 @@ Send to A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds My wallets + Это способ измерения комиссии за отправку биткоин-транзакции. Он указывает на количество самой маленькой единицы биткоина (сатоши) за каждый байт данных в транзакции. Чем выше это число, тем быстрее будет обработана транзакция сетью. + Satoshi per vbyte Sending... Tap any field to change it Send %s diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt index b8da006bb9..6de841373c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt @@ -69,12 +69,11 @@ internal sealed class SendAlertState { } data class FeeCoverage( - val amount: String, override val onConfirmClick: (() -> Unit), ) : SendAlertState() { override val title: TextReference? = null override val message: TextReference = - resourceReference(id = R.string.send_alert_fee_coverage_title, wrappedList(amount)) + resourceReference(id = R.string.send_alert_fee_coverage_title) override val confirmButtonText: TextReference = resourceReference(id = R.string.send_alert_fee_coverage_subract_text) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt index 1798d8fe79..d99b6bd0ff 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt @@ -3,7 +3,6 @@ package com.tangem.features.send.impl.presentation.state import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState @@ -50,18 +49,10 @@ internal class SendEventStateFactory( } fun getFeeCoverageAlert(onConsume: () -> Unit): SendUiState { - val state = currentStateProvider() - val amount = state.feeState?.fee?.amount ?: return state - val feeAmount = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = amount.value, - cryptoCurrency = state.cryptoCurrencySymbol, - decimals = amount.decimals, - ) - return state.copy( + return currentStateProvider().copy( event = triggeredEvent( data = SendEvent.ShowAlert( SendAlertState.FeeCoverage( - amount = feeAmount, onConfirmClick = clickIntents::onSubtractSelect, ), ), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index c8e1d05950..e49450d38b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -52,6 +52,7 @@ internal sealed class SendStates { val notifications: ImmutableList, val appCurrencyCode: String, val isFeeLoading: Boolean, + val subtractedFee: BigDecimal?, ) : SendStates() /** Recipient state */ diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt index d12d7d3c28..f06a20bf48 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt @@ -41,6 +41,7 @@ internal class SendAmountStateConverter( notifications = persistentListOf(), isFeeLoading = false, appCurrencyCode = appCurrency.code, + subtractedFee = null, segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) { persistentListOf() } else { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt index bcad6f7468..bf0f52dd73 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt @@ -15,14 +15,16 @@ internal class SendAmountSubtractConverter( val state = currentStateProvider() val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val amountState = state.amountState ?: return state - val feeValue = state.feeState?.fee?.amount?.value ?: return state + val feeState = state.feeState ?: return state + val feeValue = feeState.fee?.amount?.value ?: return state val amountTextField = amountState.amountTextField val amountValue = amountTextField.cryptoAmount.value ?: return state val fiatRate = cryptoCurrencyStatus.value.fiatRate val cryptoDecimals = amountTextField.cryptoAmount.decimals val fiatDecimals = amountTextField.fiatAmount.decimals - val decimalCryptoValue = amountValue.minus(feeValue) + val feeDiff = amountState.subtractedFee?.let { feeValue.minus(it) } ?: feeValue + val decimalCryptoValue = amountValue.minus(feeDiff) if (decimalCryptoValue < BigDecimal.ZERO) return state @@ -33,6 +35,7 @@ internal class SendAmountSubtractConverter( return state.copy( sendState = state.sendState?.copy(isSubtract = true), amountState = amountState.copy( + subtractedFee = feeValue, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt index 06a47de259..0d8e232875 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt @@ -13,7 +13,7 @@ internal fun checkFeeCoverage(state: SendUiState, cryptoCurrencyStatus: CryptoCu val balance = cryptoCurrencyStatus.value.amount ?: return false val fee = state.feeState?.fee?.amount?.value ?: return false val amount = state.amountState?.amountTextField?.cryptoAmount?.value ?: return false - return balance <= amount + fee + return balance < amount + fee } /** diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt index e42f25a9d5..8226a67504 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt @@ -59,8 +59,8 @@ internal class BitcoinCustomFeeConverter( ).toString(), decimals = SATOSHI_DECIMALS, symbol = "", - title = resourceReference(R.string.send_gas_price), - footer = resourceReference(R.string.send_gas_price_footer), + title = resourceReference(R.string.send_satoshi_per_byte_title), + footer = resourceReference(R.string.send_satoshi_per_byte_text), onValueChange = { clickIntents.onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) }, keyboardOptions = KeyboardOptions( imeAction = if (checkExceedBalance(feeValue)) ImeAction.None else ImeAction.Done, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index a1fc8fbe25..8d7daa1665 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -38,6 +38,7 @@ internal class SendAmountFieldChangeConverter( return state.copy( amountState = amountState.copy( isPrimaryButtonEnabled = !isExceedBalance && !isZero, + subtractedFee = null, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt index b29459e757..715d3ad4f8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt @@ -34,6 +34,7 @@ internal class SendAmountFieldMaxAmountConverter( return state.copy( amountState = amountState.copy( isPrimaryButtonEnabled = true, + subtractedFee = null, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt index 75e5ce97f4..91dbbde9bf 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt @@ -25,6 +25,7 @@ internal object AmountStatePreviewData { notifications = persistentListOf(), isFeeLoading = false, appCurrencyCode = "usd", + subtractedFee = null, amountTextField = SendTextField.AmountField( value = "123.123123123123123123", onValueChange = {}, From 83e6ed617144ef63d0d71c7e986f6ac37993b368 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Apr 2024 19:03:10 +0100 Subject: [PATCH 094/109] Updated on 2026-08-14 --- .../analytics/paramsInterceptor/CardContextInterceptor.kt | 4 +--- .../tangem/tap/features/welcome/redux/WelcomeMiddleware.kt | 7 ++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 6a963e544f..c881cd0127 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -15,7 +15,7 @@ import com.tangem.tap.features.demo.DemoHelper [REDACTED_AUTHOR] */ class CardContextInterceptor( - private val scanResponse: ScanResponse?, + private val scanResponse: ScanResponse, ) : ParamsInterceptor { override fun id(): String = CardContextInterceptor.id() @@ -28,8 +28,6 @@ class CardContextInterceptor( } override fun intercept(params: MutableMap) { - scanResponse ?: return - val card = scanResponse.card params[AnalyticsParam.BATCH] = card.batchId params[AnalyticsParam.PRODUCT_TYPE] = getProductType(scanResponse) diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index e2ff46f387..b4009829f8 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -18,10 +18,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockTy import com.tangem.domain.wallets.legacy.unlockIfLockable import com.tangem.tap.* import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.extensions.onUserWalletSelected +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler @@ -141,7 +138,7 @@ internal class WelcomeMiddleware { val currency = ParamCardCurrencyConverter().convert( value = scanResponse.cardTypesResolver, ) - + Analytics.addContext(scanResponse) if (currency != null) { Analytics.send( event = Basic.SignedIn( From 00c8254540320c88410a94edecec4a379d0084fa Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Apr 2024 17:17:39 +0500 Subject: [PATCH 095/109] Updated on 2026-08-14 --- .../presentation/state/confirm/SendNotificationFactory.kt | 7 ++++--- .../com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 6a4e8a383e..fb65e2a8b5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.common.extensions.minimalAmount import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -225,13 +226,14 @@ internal class SendNotificationFactory( val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO val isTezos = cryptoCurrencyStatus.currency.network.id.value == Blockchain.Tezos.id + val threshold = Blockchain.Tezos.minimalAmount() val isTotalBalance = feeAmount.plus(sendAmount) >= balance if (!ignoreAmountReduce && isTotalBalance && isTezos) { add( SendNotification.Warning.HighFeeError( - amount = TEZOS_FEE_THRESHOLD.toPlainString(), + amount = threshold.toPlainString(), onConfirmClick = { - val reduceTo = sendAmount.minus(TEZOS_FEE_THRESHOLD).toPlainString() + val reduceTo = sendAmount.minus(threshold).toPlainString() clickIntents.onAmountReduceClick(reduceTo, SendNotification.Warning.HighFeeError::class.java) }, onCloseClick = { @@ -372,7 +374,6 @@ internal class SendNotificationFactory( companion object { private const val DOGECOIN_MINIMUM = "0.01" - private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01") internal val FEE_MAX_DIFF = BigInteger("5") } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 966b619bf8..0b77da443d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -10,6 +10,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.domain.common.extensions.minimalAmount import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -433,7 +434,7 @@ internal class SwapInteractorImpl @Inject constructor( ) { val isTezos = fromTokenStatus.currency.network.id.value == Blockchain.Tezos.id if (isTezos && amount.value == fromTokenStatus.value.amount) { - warnings.add(Warning.ReduceAmountWarning(TEZOS_FEE_THRESHOLD)) + warnings.add(Warning.ReduceAmountWarning(Blockchain.Tezos.minimalAmount())) } } @@ -1595,6 +1596,5 @@ internal class SwapInteractorImpl @Inject constructor( private const val INCREASE_GAS_LIMIT_BY = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% private const val INFINITY_SYMBOL = "∞" - private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01") } } \ No newline at end of file From 80e007c6b6c1333a2a9ffbc6cad6dcbaab38e8b8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Apr 2024 17:18:44 +0500 Subject: [PATCH 096/109] Updated on 2026-08-14 --- .../features/send/impl/presentation/viewmodel/SendViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 8564920606..82ab855b4d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -647,8 +647,7 @@ internal class SendViewModel @Inject constructor( private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) { val isRecent = type == EnterAddressSource.RecentAddress - val isAddressOnly = uiState.recipientState?.memoTextField == null - if (isRecent && isAddressOnly && isValidAddress) onNextClick() + if (isRecent && isValidAddress) onNextClick() } // endregion From 16c1a70a4b3a511a1be5a3c9d9cc51ed58d2150a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Apr 2024 17:19:58 +0500 Subject: [PATCH 097/109] Updated on 2026-08-14 --- .../send/impl/presentation/state/SendStateFactory.kt | 6 +++--- .../features/send/impl/presentation/state/SendUiState.kt | 2 +- .../tangem/features/send/impl/presentation/ui/SendScreen.kt | 2 +- .../features/send/impl/presentation/ui/send/SendContent.kt | 6 +----- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 7a5c113219..24be0a0973 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -92,7 +92,7 @@ internal class SendStateFactory( event = consumedEvent(), isEditingDisabled = false, isBalanceHidden = false, - cryptoCurrencySymbol = "", + cryptoCurrencyName = "", ) fun getReadyState(): SendUiState { @@ -103,7 +103,7 @@ internal class SendStateFactory( ?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)), feeState = state.feeState ?: feeStateConverter.convert(Unit), sendState = confirmStateConverter.convert(Unit), - cryptoCurrencySymbol = cryptoCurrencyStatusProvider().currency.symbol, + cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name, ) } @@ -115,7 +115,7 @@ internal class SendStateFactory( ?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)), feeState = state.feeState ?: feeStateConverter.convert(Unit), isEditingDisabled = true, - cryptoCurrencySymbol = cryptoCurrencyStatusProvider().currency.symbol, + cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index e49450d38b..586c55c978 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -23,7 +23,7 @@ import java.math.BigDecimal internal data class SendUiState( val clickIntents: SendClickIntents, val isEditingDisabled: Boolean, - val cryptoCurrencySymbol: String, + val cryptoCurrencyName: String, val amountState: SendStates.AmountState? = null, val recipientState: SendStates.RecipientState? = null, val feeState: SendStates.FeeState? = null, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 2dcff9830c..0294fefa3b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -43,7 +43,7 @@ internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) SendUiStateType.Recipient -> resourceReference(R.string.send_recipient_label) SendUiStateType.Fee -> resourceReference(R.string.common_fee_selector_title) SendUiStateType.Send -> if (!sendState.isSuccess) { - resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencySymbol)) + resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencyName)) } else { null } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 0c92c24fd5..78809756c4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -35,11 +35,7 @@ internal fun SendContent(uiState: SendUiState) { LazyColumn( modifier = Modifier .fillMaxSize() - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), + .padding(horizontal = TangemTheme.dimens.spacing16), ) { blocks(uiState) tapHelp(isDisplay = sendState.showTapHelp) From 4538e9f03be30a21befe8ba05bebef880264ea22 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Apr 2024 15:14:39 +0100 Subject: [PATCH 098/109] Updated on 2026-08-14 --- .../java/com/tangem/datasource/config/ConfigManagerImpl.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index d26cc5e36e..d639cfbe6a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -146,9 +146,9 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { blockBookRest = accessTokens.bitcoin?.blockBookRest, ), algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest), - zkSync = GetBlockAccessToken(rest = accessTokens.zksync?.rest), - polygonZkevm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.rest), - base = GetBlockAccessToken(rest = accessTokens.base?.rest), + zkSync = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC), + polygonZkevm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC), + base = GetBlockAccessToken(rest = accessTokens.base?.jsonRPC), ) } } From fad7c0043dad63eb774b36e163e7c962183564a4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Apr 2024 19:48:36 +0500 Subject: [PATCH 099/109] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 2 +- .../features/send/impl/presentation/state/fee/FeeCalculation.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index a92316eb37..1251772f83 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -482,7 +482,7 @@ Send to A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds My wallets - Это способ измерения комиссии за отправку биткоин-транзакции. Он указывает на количество самой маленькой единицы биткоина (сатоши) за каждый байт данных в транзакции. Чем выше это число, тем быстрее будет обработана транзакция сетью. + The fee for a Bitcoin transaction is measured by the number of the smallest Bitcoin unit (Satoshi) per byte of data. The higher this number, the faster the transaction will be processed. Satoshi per vbyte Sending... Tap any field to change it diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt index 0d8e232875..57e567a586 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt @@ -13,7 +13,7 @@ internal fun checkFeeCoverage(state: SendUiState, cryptoCurrencyStatus: CryptoCu val balance = cryptoCurrencyStatus.value.amount ?: return false val fee = state.feeState?.fee?.amount?.value ?: return false val amount = state.amountState?.amountTextField?.cryptoAmount?.value ?: return false - return balance < amount + fee + return balance < amount + fee && balance > fee } /** From 7d15269083699a7d55aef1b4d3e9c8d28e10b0d2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Apr 2024 17:20:20 +0500 Subject: [PATCH 100/109] Updated on 2026-08-14 --- .../repository/DefaultTxHistoryRepository.kt | 15 +++++++++++++++ .../txhistory/repository/TxHistoryRepository.kt | 3 +++ .../usecase/GetExplorerTransactionUrlUseCase.kt | 15 +++++++++++++++ .../impl/presentation/state/SendStateFactory.kt | 9 +-------- .../impl/presentation/viewmodel/SendViewModel.kt | 13 ++++++++++--- 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index dba0ed594a..64268e3c0c 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -9,6 +9,7 @@ import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource import com.tangem.datasource.local.txhistory.TxHistoryItemsStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.txhistory.models.Page @@ -69,6 +70,7 @@ class DefaultTxHistoryRepository( return pager.flow } + @Deprecated("Replace with getTxExploreUrl [UserWalletId, Network] instead") override fun getTxExploreUrl(txHash: String, networkId: Network.ID): String { val blockchain = Blockchain.fromId(networkId.value) return when (val txExploreState = blockchain.getExploreTxUrl(txHash)) { @@ -77,6 +79,19 @@ class DefaultTxHistoryRepository( } } + override suspend fun getTxExploreUrl(userWalletId: UserWalletId, network: Network): String { + val blockchain = Blockchain.fromNetworkId(network.id.value) + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + network = network, + ) + val lastTxHash = walletManager?.wallet?.recentTransactions?.last()?.hash.orEmpty() + return when (val txExploreState = blockchain?.getExploreTxUrl(lastTxHash)) { + is TxExploreState.Url -> txExploreState.url + else -> "" + } + } + override suspend fun getFixedSizeTxHistoryItems( userWalletId: UserWalletId, currency: CryptoCurrency, diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt index b010a54db4..652b15c85b 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt @@ -24,6 +24,9 @@ interface TxHistoryRepository { fun getTxExploreUrl(txHash: String, networkId: Network.ID): String + /** Get transaction url in explorer via last transaction from wallet's recentTransactions list */ + suspend fun getTxExploreUrl(userWalletId: UserWalletId, network: Network): String + @Throws(TxHistoryListError::class) suspend fun getFixedSizeTxHistoryItems( userWalletId: UserWalletId, diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt index 974d439c7e..cedb309901 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt @@ -6,10 +6,12 @@ import arrow.core.raise.either import com.tangem.domain.tokens.model.Network import com.tangem.domain.txhistory.models.TxStatusError import com.tangem.domain.txhistory.repository.TxHistoryRepository +import com.tangem.domain.wallets.models.UserWalletId class GetExplorerTransactionUrlUseCase( private val repository: TxHistoryRepository, ) { + @Deprecated("Replace with invoke [UserWalletId, Network]") operator fun invoke(txHash: String, networkId: Network.ID): Either { return either { catch( @@ -22,4 +24,17 @@ class GetExplorerTransactionUrlUseCase( ) } } + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either { + return either { + catch( + block = { + repository.getTxExploreUrl(userWalletId, network).ifEmpty { + raise(TxStatusError.EmptyUrlError) + } + }, + catch = { raise(TxStatusError.DataError(it)) }, + ) + } + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 24be0a0973..6861d18b0f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -8,7 +8,6 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryItem -import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase import com.tangem.features.send.impl.R @@ -36,7 +35,6 @@ internal class SendStateFactory( private val feeCryptoCurrencyStatusProvider: Provider, private val isTapHelpPreviewEnabledProvider: Provider, private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, - private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) @@ -251,14 +249,9 @@ internal class SendStateFactory( ) } - fun getTransactionSendState(txData: TransactionData): SendUiState { + fun getTransactionSendState(txData: TransactionData, txUrl: String): SendUiState { val state = currentStateProvider() - val cryptoCurrency = cryptoCurrencyStatusProvider().currency val sendState = state.sendState ?: return state - val txUrl = getExplorerTransactionUrlUseCase( - txHash = txData.hash.orEmpty(), - networkId = cryptoCurrency.network.id, - ).getOrElse { "" } return state.copy( sendState = sendState.copy( transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 82ab855b4d..9f1089fccf 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -89,9 +89,9 @@ internal class SendViewModel @Inject constructor( private val parseQrCodeUseCase: ParseQrCodeUseCase, private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase, private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, currencyChecksRepository: CurrencyChecksRepository, isFeeApproximateUseCase: IsFeeApproximateUseCase, - getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase, getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, savedStateHandle: SavedStateHandle, @@ -123,7 +123,6 @@ internal class SendViewModel @Inject constructor( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, validateWalletMemoUseCase = validateWalletMemoUseCase, - getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase, isTapHelpPreviewEnabledProvider = Provider { isTapHelpPreviewEnabled }, ) @@ -845,13 +844,21 @@ internal class SendViewModel @Inject constructor( }, ifRight = { uiState = stateFactory.getSendingStateUpdate(isSending = false) - uiState = stateFactory.getTransactionSendState(txData) + updateTransactionStatus(txData) scheduleBalanceUpdate() analyticsEventHandler.send(SendAnalyticEvents.TransactionScreenOpened) }, ) } + private suspend fun updateTransactionStatus(txData: TransactionData) { + val txUrl = getExplorerTransactionUrlUseCase( + userWalletId = userWalletId, + network = cryptoCurrency.network, + ).getOrElse { "" } + uiState = stateFactory.getTransactionSendState(txData, txUrl) + } + private fun scheduleBalanceUpdate() { viewModelScope.launch(dispatchers.io) { delay(BALANCE_UPDATE_DELAY) From c747f6f38446ae95a718a00b1747973fba648e4c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Apr 2024 13:49:08 +0100 Subject: [PATCH 101/109] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index c3c879c727..7a8e690d21 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -25,6 +25,6 @@ }, { "name": "WC_SOLANA_TX_SIGN_ENABLED", - "version": "5.9.0" + "version": "5.11.0" } ] From 088295ceaf3995de7e8af4762d5fbc66f6eee27f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Apr 2024 17:56:58 +0500 Subject: [PATCH 102/109] Updated on 2026-08-14 --- .../core/ui/components/fields/SimpleTextField.kt | 14 ++++++-------- .../state/confirm/SendNotificationFactory.kt | 12 +----------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt index ed91b52667..b9cba56550 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -72,14 +72,12 @@ fun SimpleTextField( } var lastTextValue by remember(value) { - derivedStateOf { - val isSelectionLastIndex = textFieldValueState.selection.end == textFieldValueState.text.lastIndex - if (textFieldValueState.text.isBlank() || isSelectionLastIndex) { - textFieldValueState = textFieldValueState.copy( - text = value, - selection = getValueRange(value), - ) - } + val isSelectionLastIndex = textFieldValueState.selection.end == textFieldValueState.text.lastIndex + if (textFieldValueState.text.isBlank() || isSelectionLastIndex) { + textFieldValueState = textFieldValueState.copy( + text = value, + selection = getValueRange(value), + ) } mutableStateOf(value) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index fb65e2a8b5..68d334e0ae 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -59,7 +59,6 @@ internal class SendNotificationFactory( // errors addExceedBalanceNotification(feeAmount, amountValue) addExceedsBalanceNotification(feeState.fee) - addInvalidAmountNotification(sendState.isSubtract, amountValue) addMinimumAmountErrorNotification(feeAmount, amountValue) addDustWarningNotification(feeAmount, amountValue) addTransactionLimitErrorNotification(feeAmount, amountValue) @@ -106,15 +105,6 @@ internal class SendNotificationFactory( } } - private fun MutableList.addInvalidAmountNotification( - isSubtractAmount: Boolean, - receivedAmount: BigDecimal, - ) { - if (isSubtractAmount && receivedAmount <= BigDecimal.ZERO) { - add(SendNotification.Error.InvalidAmount) - } - } - private fun MutableList.addMinimumAmountErrorNotification( feeAmount: BigDecimal, receivedAmount: BigDecimal, @@ -227,7 +217,7 @@ internal class SendNotificationFactory( val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO val isTezos = cryptoCurrencyStatus.currency.network.id.value == Blockchain.Tezos.id val threshold = Blockchain.Tezos.minimalAmount() - val isTotalBalance = feeAmount.plus(sendAmount) >= balance + val isTotalBalance = feeAmount.plus(sendAmount) >= balance && balance > threshold if (!ignoreAmountReduce && isTotalBalance && isTezos) { add( SendNotification.Warning.HighFeeError( From 65f956fddf9f35442f2a1cc2f32068c2da3a5c4c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Apr 2024 19:16:12 +0500 Subject: [PATCH 103/109] Updated on 2026-08-14 --- .../presentation/viewmodel/SendViewModel.kt | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 9f1089fccf..c92d4b895d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.viewmodel +import android.os.SystemClock import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -191,6 +192,7 @@ internal class SendViewModel @Inject constructor( private var qrScannerJobHolder = JobHolder() private var sendIdleTimer = 0L + private var feeIdleTimer = 0L init { subscribeOnCurrencyStatusUpdates() @@ -444,7 +446,7 @@ internal class SendViewModel @Inject constructor( .onEach { when (it.type) { SendUiStateType.Fee -> loadFee() - SendUiStateType.Send -> sendIdleTimer = System.currentTimeMillis() + SendUiStateType.Send -> sendIdleTimer = SystemClock.elapsedRealtime() else -> Unit } } @@ -683,6 +685,9 @@ internal class SendViewModel @Inject constructor( } private fun loadFee(isToNextState: Boolean = false) { + // debouncing fee request + if (SystemClock.elapsedRealtime() - feeIdleTimer < FEE_UPDATE_DELAY) return + viewModelScope.launch(dispatchers.main) { val isShowStatus = uiState.feeState?.fee == null if (isShowStatus) { @@ -690,6 +695,7 @@ internal class SendViewModel @Inject constructor( } val result = callFeeUseCase()?.fold( ifRight = { + feeIdleTimer = SystemClock.elapsedRealtime() uiState = feeStateFactory.onFeeOnLoadedState(it) if (isToNextState && !onFeeCoverageAlert()) { stateRouter.showSend() @@ -747,12 +753,13 @@ internal class SendViewModel @Inject constructor( if (sendState.isSuccess) popBackStack() uiState = stateFactory.getSendingStateUpdate(isSending = true) - if (System.currentTimeMillis() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) { + if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) { verifyAndSendTransaction() } else { onCheckFeeUpdate() + feeIdleTimer = SystemClock.elapsedRealtime() + sendIdleTimer = SystemClock.elapsedRealtime() } - sendIdleTimer = System.currentTimeMillis() } override fun showAmount() { @@ -917,13 +924,14 @@ internal class SendViewModel @Inject constructor( } // endregion - companion object { - private const val CHECK_FEE_UPDATE_DELAY = 60_000L - private const val BALANCE_UPDATE_DELAY = 10_000L + private companion object { + const val CHECK_FEE_UPDATE_DELAY = 60_000L + const val FEE_UPDATE_DELAY = 10_000L + const val BALANCE_UPDATE_DELAY = 10_000L - private const val RU_LOCALE = "ru" - private const val EN_LOCALE = "en" - private const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/" - private const val FEE_READ_MORE_URL_SECOND_PART = "/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it/" + const val RU_LOCALE = "ru" + const val EN_LOCALE = "en" + const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/" + const val FEE_READ_MORE_URL_SECOND_PART = "/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it/" } } \ No newline at end of file From ce82e49ef4a58365e2f352ca56e4efd877497e83 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Apr 2024 12:15:22 +0500 Subject: [PATCH 104/109] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 2 +- .../usecase/GetFixedTxHistoryItemsUseCase.kt | 11 +++ .../domain/SendRecipientListContent.kt | 5 +- .../presentation/state/SendStateFactory.kt | 49 ++++++++-- ...t => SendRecipientHistoryListConverter.kt} | 49 +++------- .../recipient/SendRecipientStateConverter.kt | 6 +- .../SendRecipientWalletListConverter.kt | 39 ++++++++ .../state/recipient/utils/RecentListUtils.kt | 32 ++++++ .../ui/recipient/ListItemWithIcon.kt | 97 ++++++++++++++++++- .../ui/recipient/SendRecipientContent.kt | 58 +++++------ .../presentation/viewmodel/SendViewModel.kt | 66 ++++++------- 11 files changed, 294 insertions(+), 120 deletions(-) rename features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/{SendRecipientListConverter.kt => SendRecipientHistoryListConverter.kt} (71%) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/utils/RecentListUtils.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 1251772f83..9310a0153a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -422,7 +422,7 @@ Already included in the entered address Subtract Not enough funds to cover the network fee. Do you want to subtract the amount required to cover the fee? - The commission amount is %@ times the recommended amount. Make sure that the custom settings are correct. + The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue? Reason: %1$s\nCode: %2$s The transaction is not completed diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt index aa93bec246..8b5a463618 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt @@ -33,4 +33,15 @@ class GetFixedTxHistoryItemsUseCase( } }.mapLeft { TxHistoryListError.DataError(it) } } + + suspend fun getSync( + userWalletId: UserWalletId, + currency: CryptoCurrency, + pageSize: Int = DEFAULT_PAGE_SIZE, + refresh: Boolean = false, + ): Either> { + return Either.catch { + repository.getFixedSizeTxHistoryItems(userWalletId, currency, pageSize, refresh) + }.mapLeft { TxHistoryListError.DataError(it) } + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt index 0837f1f27c..03e9414c0a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt @@ -5,10 +5,11 @@ import com.tangem.core.ui.extensions.TextReference data class SendRecipientListContent( val id: String, - val title: TextReference, - val subtitle: TextReference, + val title: TextReference = TextReference.EMPTY, + val subtitle: TextReference = TextReference.EMPTY, val timestamp: TextReference? = null, val subtitleEndOffset: Int = 0, @DrawableRes val subtitleIconRes: Int? = null, val isVisible: Boolean = true, + val isLoading: Boolean = false, ) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 6861d18b0f..f3f304b7ff 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -4,6 +4,7 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -17,12 +18,14 @@ import com.tangem.features.send.impl.presentation.state.amount.SendAmountSubtrac import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter -import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter +import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientHistoryListConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter +import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientWalletListConverter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import timber.log.Timber @Suppress("LongParameterList") @@ -77,9 +80,11 @@ internal class SendStateFactory( isTapHelpPreviewEnabledProvider = isTapHelpPreviewEnabledProvider, ) } - private val recipientListStateConverter by lazy(LazyThreadSafetyMode.NONE) { - SendRecipientListConverter( - currentStateProvider = currentStateProvider, + private val recipientWalletListStateConverter by lazy(LazyThreadSafetyMode.NONE) { + SendRecipientWalletListConverter() + } + private val recipientHistoryListStateConverter by lazy(LazyThreadSafetyMode.NONE) { + SendRecipientHistoryListConverter( cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } @@ -123,11 +128,23 @@ internal class SendStateFactory( //endregion //region recipient - fun onLoadedRecipientList(wallets: List, txHistory: List): SendUiState = - recipientListStateConverter.convert( - wallets = wallets, - txHistory = txHistory, + fun onLoadedWalletsList(wallets: List): SendUiState { + val state = currentStateProvider() + return state.copy( + recipientState = state.recipientState?.copy( + wallets = recipientWalletListStateConverter.convert(wallets), + ), ) + } + + fun onLoadedHistoryList(txHistory: List): SendUiState { + val state = currentStateProvider() + return state.copy( + recipientState = state.recipientState?.copy( + recent = recipientHistoryListStateConverter.convert(txHistory), + ), + ) + } fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false): SendUiState { val state = currentStateProvider() @@ -228,6 +245,22 @@ internal class SendStateFactory( ), ) } + + fun getHiddenRecentListState(isAddressInWallet: Boolean, isValidAddress: Boolean): SendUiState { + val state = currentStateProvider() + val recipientState = state.recipientState ?: return state + val isNotValid = isAddressInWallet || !isValidAddress + return state.copy( + recipientState = recipientState.copy( + recent = recipientState.recent.map { recent -> + recent.copy(isVisible = isNotValid && (recent.isLoading || recent.title != TextReference.EMPTY)) + }.toPersistentList(), + wallets = recipientState.wallets.map { wallet -> + wallet.copy(isVisible = isNotValid && (wallet.isLoading || wallet.title != TextReference.EMPTY)) + }.toPersistentList(), + ), + ) + } //endregion //region send diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt similarity index 71% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt rename to features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt index 046c7a153d..f6ccb6ba98 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt @@ -10,49 +10,26 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent -import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.recipient.utils.RECENT_DEFAULT_COUNT +import com.tangem.features.send.impl.presentation.state.recipient.utils.RECENT_KEY_TAG +import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyListState import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter import com.tangem.utils.toFormattedCurrencyString +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList -internal class SendRecipientListConverter( - private val currentStateProvider: Provider, +internal class SendRecipientHistoryListConverter( private val cryptoCurrencyStatusProvider: Provider, -) { +) : Converter, ImmutableList> { - fun convert(wallets: List, txHistory: List): SendUiState { + override fun convert(value: List): ImmutableList { val cryptoCurrency = cryptoCurrencyStatusProvider().currency - val state = currentStateProvider() - val recipientState = state.recipientState ?: return state - - return state.copy( - recipientState = recipientState.copy( - wallets = wallets.filterWallets(), - recent = txHistory.filterRecipients(cryptoCurrency), - ), - ) - } - - private fun List.filterWallets() = this.filterNotNull() - .groupBy { item -> item.name } - .values.map { - it.mapIndexed { index, item -> - val name = if (it.size > 1) { - "${item.name} ${index.inc()}" - } else { - item.name - } - SendRecipientListContent( - id = item.address, - title = TextReference.Str(item.address), - subtitle = TextReference.Str(name), - ) - } + return value.filterRecipients(cryptoCurrency).ifEmpty { + emptyListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT) } - .flatten() - .toPersistentList() + } private fun List.filterRecipients(cryptoCurrency: CryptoCurrency) = this.filter { item -> val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer @@ -65,9 +42,9 @@ internal class SendRecipientListConverter( isTransfer && isSingleAddress && isNotContract } .take(RECENT_LIST_SIZE) - .map { tx -> + .mapIndexed { index, tx -> SendRecipientListContent( - id = tx.txHash, + id = "$RECENT_KEY_TAG$index", title = tx.extractAddress(), subtitle = stringReference(tx.getAmount(cryptoCurrency).trim()), timestamp = tx.extractTimestamp(), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt index dd7db52c45..9b3f770fcf 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt @@ -2,10 +2,10 @@ package com.tangem.features.send.impl.presentation.state.recipient import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.recipient.utils.* import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.persistentListOf internal class SendRecipientStateConverter( private val clickIntents: SendClickIntents, @@ -26,8 +26,8 @@ internal class SendRecipientStateConverter( memoTextField = memoFieldConverter.convertOrNull(value.memo), network = cryptoCurrencyStatusProvider().currency.network.name, isPrimaryButtonEnabled = false, - wallets = persistentListOf(), - recent = persistentListOf(), + wallets = loadingListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT), + recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt new file mode 100644 index 0000000000..855e5d3a3b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt @@ -0,0 +1,39 @@ +package com.tangem.features.send.impl.presentation.state.recipient + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.send.impl.presentation.domain.AvailableWallet +import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent +import com.tangem.features.send.impl.presentation.state.recipient.utils.WALLET_DEFAULT_COUNT +import com.tangem.features.send.impl.presentation.state.recipient.utils.WALLET_KEY_TAG +import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyListState +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList + +internal class SendRecipientWalletListConverter : + Converter, PersistentList> { + override fun convert(value: List): PersistentList { + return value.filterWallets().ifEmpty { + emptyListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT) + } + } + + private fun List.filterWallets() = this.filterNotNull() + .groupBy { item -> item.name } + .values.map { + it.mapIndexed { index, item -> + val name = if (it.size > 1) { + "${item.name} ${index.inc()}" + } else { + item.name + } + SendRecipientListContent( + id = "${WALLET_KEY_TAG}$index", + title = TextReference.Str(item.address), + subtitle = TextReference.Str(name), + ) + } + } + .flatten() + .toPersistentList() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/utils/RecentListUtils.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/utils/RecentListUtils.kt new file mode 100644 index 0000000000..c2d5ad9739 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/utils/RecentListUtils.kt @@ -0,0 +1,32 @@ +package com.tangem.features.send.impl.presentation.state.recipient.utils + +import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent +import kotlinx.collections.immutable.toPersistentList + +internal const val WALLET_DEFAULT_COUNT = 1 +internal const val RECENT_DEFAULT_COUNT = 3 +internal const val WALLET_KEY_TAG = "wallet" +internal const val RECENT_KEY_TAG = "recent" + +internal fun loadingListState(tag: String, count: Int) = buildList { + repeat(count) { + add( + SendRecipientListContent( + id = "$tag$it", + isLoading = true, + ), + ) + } +}.toPersistentList() + +internal fun emptyListState(tag: String, count: Int) = buildList { + repeat(count) { + add( + SendRecipientListContent( + id = "$tag$it", + isLoading = false, + isVisible = false, + ), + ) + } +}.toPersistentList() \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt index c33885f290..9ef9f60a21 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt @@ -1,6 +1,10 @@ package com.tangem.features.send.impl.presentation.ui.recipient import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -9,6 +13,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource @@ -16,6 +21,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.icons.identicon.IdentIcon @@ -43,9 +50,42 @@ fun ListItemWithIcon( info: String? = null, subtitleEndOffset: Int = 0, @DrawableRes subtitleIconRes: Int? = null, + isLoading: Boolean = false, +) { + AnimatedContent( + targetState = isLoading, + label = "Recent List Content Animation", + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + ) { isLoadingState -> + if (isLoadingState) { + ListItemLoading(modifier = modifier) + } else { + ListItemWithIcon( + title = title, + subtitle = subtitle, + onClick = onClick, + info = info, + subtitleEndOffset = subtitleEndOffset, + subtitleIconRes = subtitleIconRes, + modifier = modifier, + ) + } + } +} + +@Composable +private fun ListItemWithIcon( + title: String, + subtitle: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + info: String? = null, + subtitleEndOffset: Int = 0, + @DrawableRes subtitleIconRes: Int? = null, ) { val hapticFeedback = rememberHapticFeedback(state = title, onAction = onClick) Row( + verticalAlignment = Alignment.CenterVertically, modifier = modifier .fillMaxWidth() .clickable { hapticFeedback() } @@ -55,13 +95,12 @@ fun ListItemWithIcon( address = title, modifier = Modifier .padding(vertical = TangemTheme.dimens.spacing8) - .size(TangemTheme.dimens.size40) - .clip(RoundedCornerShape(TangemTheme.dimens.radius20)), + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius18)), ) Column( - modifier = Modifier - .padding(vertical = TangemTheme.dimens.spacing10) - .padding(start = TangemTheme.dimens.spacing12), + modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.SpaceBetween, ) { EllipsisText( text = title, @@ -102,6 +141,43 @@ fun ListItemWithIcon( } } +@Composable +private fun ListItemLoading(modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing12), + ) { + CircleShimmer( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing8) + .size(TangemTheme.dimens.size36), + ) + Column( + modifier = Modifier + .height(TangemTheme.dimens.size32) + .padding(start = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.SpaceBetween, + ) { + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier.size( + width = TangemTheme.dimens.spacing70, + height = TangemTheme.dimens.spacing12, + ), + ) + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier.size( + width = TangemTheme.dimens.spacing52, + height = TangemTheme.dimens.spacing12, + ), + ) + } + } +} + // region preview @Preview @Composable @@ -115,6 +191,7 @@ private fun ListItemWithIconPreview_Light( subtitleEndOffset = config.subtitleEndOffset, subtitleIconRes = config.iconRes, onClick = {}, + isLoading = config.isLoading, ) } } @@ -131,6 +208,7 @@ private fun ListItemWithIconPreview_Dark( subtitleEndOffset = config.subtitleEndOffset, subtitleIconRes = config.iconRes, onClick = {}, + isLoading = config.isLoading, ) } } @@ -141,6 +219,7 @@ private data class ListItemWithIconPreviewConfig( val info: String? = null, val subtitleEndOffset: Int = 0, val iconRes: Int? = null, + val isLoading: Boolean = false, ) private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvider( @@ -163,6 +242,14 @@ private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvid title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", subtitle = "Wallet", ), + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "0.000000000000000000000000000000 BTC", + info = "0.0.0000 at 00:00", + subtitleEndOffset = "BTC".length, + iconRes = R.drawable.ic_arrow_down_24, + isLoading = true, + ), ), ) //endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index eb9b2b544a..f728ff97cb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -2,8 +2,8 @@ package com.tangem.features.send.impl.presentation.ui.recipient import androidx.annotation.StringRes import androidx.compose.animation.* -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -76,13 +76,13 @@ internal fun SendRecipientContent( listItem( list = wallets, clickIntents = clickIntents, - isLast = recipients.isEmpty(), + isLast = recipients.any { !it.isVisible }, isBalanceHidden = isBalanceHidden, ) listHeaderItem( titleRes = R.string.send_recent_transactions, isVisible = recipients.isNotEmpty() && recipients.first().isVisible, - isFirst = wallets.isEmpty(), + isFirst = wallets.any { !it.isVisible }, ) listItem( list = recipients, @@ -143,20 +143,9 @@ private fun LazyListScope.memoField(memoField: SendTextField.RecipientMemo?, onM } } -@OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Boolean, isFirst: Boolean) { - item( - key = titleRes, - ) { - AnimatedVisibility( - visible = isVisible, - label = "Header Appearance Animation", - enter = slideInVertically() + fadeIn(), - exit = slideOutVertically() + fadeOut(), - modifier = Modifier - .animateItemPlacement() - .animateContentSize(), - ) { + item(key = titleRes) { + AnimateRecentAppearance(isVisible) { val (topPadding, paddingFromTop) = if (isFirst) { TangemTheme.dimens.spacing20 to TangemTheme.dimens.spacing12 } else { @@ -192,7 +181,6 @@ private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Bo } } -@OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.listItem( list: ImmutableList, clickIntents: SendClickIntents, @@ -206,22 +194,20 @@ private fun LazyListScope.listItem( ) { index -> val item = list[index] val title = item.title.resolveReference() - AnimatedVisibility( - visible = item.isVisible, - label = "Header Appearance Animation", - enter = slideInVertically() + fadeIn(), - exit = slideOutVertically() + fadeOut(), - modifier = Modifier - .animateItemPlacement() - .animateContentSize(), - ) { + AnimateRecentAppearance(item.isVisible) { ListItemWithIcon( title = title, subtitle = if (isBalanceHidden) STARS else item.subtitle.resolveReference(), info = item.timestamp?.resolveReference(), subtitleEndOffset = item.subtitleEndOffset, subtitleIconRes = item.subtitleIconRes, - onClick = { clickIntents.onRecipientAddressValueChange(title, EnterAddressSource.RecentAddress) }, + onClick = { + clickIntents.onRecipientAddressValueChange( + title, + EnterAddressSource.RecentAddress, + ) + }, + isLoading = item.isLoading, modifier = Modifier .then( if (isLast && index == list.lastIndex) { @@ -240,4 +226,22 @@ private fun LazyListScope.listItem( ) } } +} + +@Composable +private fun AnimateRecentAppearance(isVisible: Boolean, content: @Composable () -> Unit) { + AnimatedContent( + targetState = isVisible, + label = "Item Appearance Animation", + transitionSpec = { + (slideInHorizontally() + fadeIn()) + .togetherWith(slideOutVertically() + fadeOut()) + }, + ) { + if (it) { + content() + } else { + Box(modifier = Modifier.fillMaxWidth()) + } + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index c92d4b895d..2a3112371e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -30,7 +30,6 @@ import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase -import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet @@ -56,7 +55,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber @@ -183,7 +181,6 @@ internal class SendViewModel @Inject constructor( private var balanceJobHolder = JobHolder() private var balanceHidingJobHolder = JobHolder() - private var recipientsJobHolder = JobHolder() private var feeJobHolder = JobHolder() private var addressValidationJobHolder = JobHolder() private var memoValidationJobHolder = JobHolder() @@ -374,34 +371,33 @@ internal class SendViewModel @Inject constructor( } private fun getWalletsAndRecent() { - combine( - flow = getUserWallets().conflate(), - flow2 = getTxHistory().conflate(), - ) { wallets, txHistory -> - uiState = stateFactory.onLoadedRecipientList( - wallets = wallets, - txHistory = txHistory, - ) + getUserWallets() + viewModelScope.launch(dispatchers.main) { + getTxHistory() } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(recipientsJobHolder) } - private fun getUserWallets(): Flow> { - return getWalletsUseCase() + private fun getUserWallets() { + getWalletsUseCase() + .conflate() .distinctUntilChanged() - .map { userWallets -> + .onEach { userWallets -> coroutineScope { - userWallets - .filterNot { it.walletId == userWalletId || it.isLocked } - .map { wallet -> - async(dispatchers.io) { - wallet.toAvailableWallet() - } - } - }.awaitAll() + runCatching { + userWallets + .filterNot { it.walletId == userWalletId || it.isLocked } + .map { wallet -> + async(dispatchers.io) { wallet.toAvailableWallet() } + }.awaitAll() + }.onSuccess { result -> + uiState = stateFactory.onLoadedWalletsList(wallets = result) + }.onFailure { + uiState = stateFactory.onLoadedWalletsList(wallets = emptyList()) + } + } } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) } private suspend fun UserWallet.toAvailableWallet(): AvailableWallet? { @@ -431,14 +427,12 @@ internal class SendViewModel @Inject constructor( } } - private fun getTxHistory(): Flow> { - return getFixedTxHistoryItemsUseCase( + private suspend fun getTxHistory() { + val txHistoryList = getFixedTxHistoryItemsUseCase.getSync( userWalletId = userWalletId, currency = cryptoCurrency, - ).fold( - ifRight = { it.distinctUntilChanged() }, - ifLeft = { emptyFlow() }, - ) + ).getOrElse { emptyList() } + uiState = stateFactory.onLoadedHistoryList(txHistory = txHistoryList) } private fun onStateActive() { @@ -636,13 +630,9 @@ internal class SendViewModel @Inject constructor( } private fun onEnteredValidAddress(isValidAddress: Boolean, isAddressInWallet: Boolean) { - val recipientState = uiState.recipientState ?: return - val isVisible = isAddressInWallet || !isValidAddress - uiState = uiState.copy( - recipientState = recipientState.copy( - recent = recipientState.recent.map { it.copy(isVisible = isVisible) }.toPersistentList(), - wallets = recipientState.wallets.map { it.copy(isVisible = isVisible) }.toPersistentList(), - ), + uiState = stateFactory.getHiddenRecentListState( + isAddressInWallet = isAddressInWallet, + isValidAddress = isValidAddress, ) } From 5dbe85c826209d9f4caea912c6bf3517bca241bd Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Apr 2024 12:59:45 +0500 Subject: [PATCH 105/109] Updated on 2026-08-14 --- .../presentation/state/fee/custom/EthereumCustomFeeConverter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt index a9bf8e5607..4851533b49 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -48,7 +48,7 @@ internal class EthereumCustomFeeConverter( keyboardActions = KeyboardActions(), ), SendTextField.CustomFee( - value = value.gasPrice.toBigDecimal().movePointLeft(GIGA_DECIMALS).toString(), + value = value.gasPrice.toBigDecimal().movePointLeft(GIGA_DECIMALS).parseBigDecimal(GIGA_DECIMALS), decimals = GIGA_DECIMALS, symbol = ETHEREUM_GAS_UNIT, title = resourceReference(R.string.send_gas_price), From fe23b8ccc319aa90cdb800b5b0c94aed08ee4e72 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Apr 2024 15:44:42 +0300 Subject: [PATCH 106/109] Updated on 2026-08-14 --- .../send/impl/presentation/ui/SendScreen.kt | 57 ++++++++++--------- .../presentation/ui/amount/AmountField.kt | 18 ++++-- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 0294fefa3b..caf0a2f4f2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -98,33 +98,36 @@ private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentS AnimatedContentTransitionScope.SlideDirection.End } } - AnimatedContent( - targetState = currentState, - label = "Send Scree Navigation", - modifier = modifier, - transitionSpec = { - lastState = currentState.type.ordinal - slideIntoContainer(towards = direction, animationSpec = tween()) - .togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween())) - }, - ) { state -> - when (state.type) { - SendUiStateType.Amount -> SendAmountContent( - amountState = uiState.amountState, - isBalanceHiding = uiState.isBalanceHidden, - clickIntents = uiState.clickIntents, - ) - SendUiStateType.Recipient -> SendRecipientContent( - uiState = uiState.recipientState, - clickIntents = uiState.clickIntents, - isBalanceHidden = uiState.isBalanceHidden, - ) - SendUiStateType.Fee -> SendSpeedAndFeeContent( - state = uiState.feeState, - clickIntents = uiState.clickIntents, - ) - SendUiStateType.Send -> SendContent(uiState) - else -> Unit + // Box is needed to fix animation with resizing of AnimatedContent + Box(modifier = modifier) { + AnimatedContent( + targetState = currentState, + label = "Send Scree Navigation", + transitionSpec = { + lastState = currentState.type.ordinal + slideIntoContainer(towards = direction, animationSpec = tween()) + .togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween())) + }, + ) { state -> + + when (state.type) { + SendUiStateType.Amount -> SendAmountContent( + amountState = uiState.amountState, + isBalanceHiding = uiState.isBalanceHidden, + clickIntents = uiState.clickIntents, + ) + SendUiStateType.Recipient -> SendRecipientContent( + uiState = uiState.recipientState, + clickIntents = uiState.clickIntents, + isBalanceHidden = uiState.isBalanceHidden, + ) + SendUiStateType.Fee -> SendSpeedAndFeeContent( + state = uiState.feeState, + clickIntents = uiState.clickIntents, + ) + SendUiStateType.Send -> SendContent(uiState) + else -> Unit + } } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt index 5aa3ec82ac..bb5b97aa51 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -7,9 +7,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment.Companion.BottomCenter import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -24,6 +22,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import kotlinx.coroutines.delay import kotlinx.coroutines.job @Composable @@ -37,6 +36,16 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea sendField.cryptoAmount to sendField.value } val requester = remember { FocusRequester() } + var isEnabledProxy by remember { mutableStateOf(isEnabled) } + + // Fix animation from amount screen to summary screen ([REDACTED_TASK_KEY]) + LaunchedEffect(key1 = isEnabled) { + if (isEnabled) { + delay(timeMillis = 700) + } + isEnabledProxy = isEnabled + } + AmountTextField( value = primaryValue, decimals = primaryAmount.decimals, @@ -53,7 +62,7 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, ), - isEnabled = isEnabled, + isEnabled = isEnabledProxy, isAutoResize = true, modifier = Modifier .focusRequester(requester) @@ -64,6 +73,7 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea ) .requiredHeightIn(min = TangemTheme.dimens.size32), ) + LaunchedEffect(key1 = Unit) { this.coroutineContext.job.invokeOnCompletion { requester.requestFocus() From a7a032b65d01d26ed3975688cd1b91805e188a91 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Apr 2024 17:43:17 +0500 Subject: [PATCH 107/109] Updated on 2026-08-14 --- .../data/txhistory/repository/DefaultTxHistoryRepository.kt | 3 +-- .../send/impl/presentation/ui/SendNavigationButtons.kt | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index 64268e3c0c..eada356d1e 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -9,7 +9,6 @@ import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource import com.tangem.datasource.local.txhistory.TxHistoryItemsStore import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.txhistory.models.Page @@ -80,7 +79,7 @@ class DefaultTxHistoryRepository( } override suspend fun getTxExploreUrl(userWalletId: UserWalletId, network: Network): String { - val blockchain = Blockchain.fromNetworkId(network.id.value) + val blockchain = Blockchain.fromId(network.id.value) val walletManager = walletManagersFacade.getOrCreateWalletManager( userWalletId = userWalletId, network = network, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 20f2800793..fda14b8a8a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -185,7 +185,7 @@ private fun SendDoneButtons( exit = slideOutVertically().plus(fadeOut()), label = "Animate show sent state buttons", ) { - Row(modifier = Modifier.padding(TangemTheme.dimens.spacing12)) { + Row(modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12)) { SecondaryButtonIconStart( text = stringResource(id = R.string.common_explore), iconResId = R.drawable.ic_web_24, From 9fe8c6bedd0efdfc4718f1e9e73c580baaa80531 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Apr 2024 14:31:04 +0100 Subject: [PATCH 108/109] Updated on 2026-08-14 --- .../java/com/tangem/tap/proxy/TransactionManagerImpl.kt | 6 +++--- gradle/dependencies.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 4c7a8f4461..8b3e444c59 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -9,7 +9,7 @@ import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder -import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager +import com.tangem.blockchain.blockchains.optimism.EthereumOptimisticRollupWalletManager import com.tangem.blockchain.blockchains.stellar.StellarMemo import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras @@ -170,7 +170,7 @@ class TransactionManagerImpl( val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } val walletManager = getActualWalletManager(blockchain, derivationPath) if (walletManager is EthereumWalletManager) { - if (walletManager is OptimismWalletManager) { + if (walletManager is EthereumOptimisticRollupWalletManager) { return getFeeForOptimismBlockchain( walletManager = walletManager, amount = createAmount(amountToSend, currencyToSend, blockchain), @@ -285,7 +285,7 @@ class TransactionManagerImpl( } private suspend fun getFeeForOptimismBlockchain( - walletManager: OptimismWalletManager, + walletManager: EthereumOptimisticRollupWalletManager, amount: Amount, destinationAddress: String, data: String?, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 8fe2844437..702d001217 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,7 +85,7 @@ leakcanary = "2.13" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.9-589" +tangemBlockchainSdk = "release-app_5.9-596" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.9-343" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From e35cd41fd2ccd2b6d1d0f0f07472960b3056dafa Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Apr 2024 15:28:55 +0100 Subject: [PATCH 109/109] Updated on 2026-08-14 --- .../tangem/tap/di/domain/TokensDomainModule.kt | 2 +- .../tap/domain/scanCard/UseCaseScanProcessor.kt | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index e4cf06239e..81d84db2d2 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -16,7 +16,7 @@ import dagger.hilt.android.scopes.ViewModelScoped @Module @InstallIn(ViewModelComponent::class) -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LargeClass") internal object TokensDomainModule { @Provides diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index 9578e1ffe1..ed777bd666 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -54,7 +54,11 @@ internal object UseCaseScanProcessor { ) = progressScope(onProgressStateChange) { val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase) val chains = buildList { - add(FailedScansCounterChain(UseCaseScanProcessor::showMaxUnsuccessfulScansReachedDialog)) + add( + FailedScansCounterChain( + { showMaxUnsuccessfulScansReachedDialog(analyticsSource) }, + ), + ) add(AnalyticsChain(Basic.CardWasScanned(analyticsSource))) add(DisclaimerChain(store, disclaimerWillShow)) add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager)) @@ -66,8 +70,14 @@ internal object UseCaseScanProcessor { ) } - private fun showMaxUnsuccessfulScansReachedDialog() { - store.dispatchDialogShow(StateDialog.ScanFailsDialog) + private fun showMaxUnsuccessfulScansReachedDialog(source: AnalyticsParam.ScreensSources) { + val scanFailsSource = when (source) { + is AnalyticsParam.ScreensSources.SignIn -> StateDialog.ScanFailsSource.SIGN_IN + is AnalyticsParam.ScreensSources.Settings -> StateDialog.ScanFailsSource.SETTINGS + is AnalyticsParam.ScreensSources.Intro -> StateDialog.ScanFailsSource.INTRO + else -> StateDialog.ScanFailsSource.MAIN + } + store.dispatchDialogShow(StateDialog.ScanFailsDialog(scanFailsSource)) } private suspend fun proceedWithException(